Qual é a razão para o padrão C considerar a recursividade da constância?

9

O padrão C99 diz em 6.5.16: 2:

Um operador de atribuição deve ter um valor modificável como seu operando esquerdo.

e em 6.3.2.1:1:

Um lvalue modificável é um lvalue que não possui um tipo de matriz, não possui um tipo incompleto, não possui um tipo qualificado de const e, se for uma estrutura ou união, não possui nenhum membro (incluindo, recursivamente, qualquer membro ou elemento de todos os agregados ou uniões contidos) com um tipo qualificado de const.

Agora, vamos considerar const structum constcampo não- com .

typedef struct S_s {
    const int _a;
} S_t;

Por padrão, o código a seguir é um comportamento indefinido (UB):

S_t s1;
S_t s2 = { ._a = 2 };
s1 = s2;

O problema semântico disso é que a entidade envolvente ( struct) deve ser considerada gravável (não somente leitura), a julgar pelo tipo declarado da entidade ( S_t s1), mas não deve ser considerada gravável pela redação do padrão (as 2 cláusulas no topo) por causa do constcampo _a. O Padrão deixa claro para um programador que lê o código que a atribuição é realmente um UB, porque é impossível dizer que sem a definição de struct S_s ... S_ttipo.

Além disso, o acesso somente leitura ao campo é imposto de forma sintática apenas de qualquer maneira. Não há como, de fato, alguns constcampos não- const structserem realmente colocados no armazenamento somente leitura. Mas essa redação do padrão proíbe o código que deliberadamente rejeita o constqualificador de campos nos procedimentos de acessador desses campos, assim ( é uma boa idéia qualificar os campos de estrutura em C? ):

(*)

#include <stdlib.h>
#include <stdio.h>

typedef struct S_s {
    const int _a;
} S_t;

S_t *
create_S(void) {
    return calloc(sizeof(S_t), 1);
}

void
destroy_S(S_t *s) {
    free(s);
}

const int
get_S_a(const S_t *s) {
    return s->_a;
}

void
set_S_a(S_t *s, const int a) {
    int *a_p = (int *)&s->_a;
    *a_p = a;
}

int
main(void) {
    S_t s1;
    // s1._a = 5; // Error
    set_S_a(&s1, 5); // OK
    S_t *s2 = create_S();
    // s2->_a = 8; // Error
    set_S_a(s2, 8); // OK

    printf("s1.a == %d\n", get_S_a(&s1));
    printf("s2->a == %d\n", get_S_a(s2));

    destroy_S(s2);
}

Portanto, por algum motivo, para que um todo structseja somente leitura, basta declará-loconst

const S_t s3;

Mas, para que um todo structseja não somente leitura, não é suficiente declará-lo sem const.

O que eu acho que seria melhor, é:

  1. Restringir a criação de não- constestruturas com constcampos e emitir um diagnóstico nesse caso. Isso deixaria claro que os structcampos contendo somente leitura são somente leitura.
  2. Definir o comportamento em caso de gravação em um constcampo pertencente a uma não- constestrutura, a fim de tornar o código acima (*) compatível com a Norma.

Caso contrário, o comportamento não é consistente e difícil de entender.

Então, qual é a razão para o Padrão C considerar o const-ness recursivamente, como ele diz?

Michael Pankov
fonte
Para ser sincero, não vejo uma pergunta lá.
Bart van Ingen Schenau
@BartvanIngenSchenau editado para adicionar a pergunta indicada no tópico no final do corpo #
Michael Pankov
11
Por que o voto negativo?
Michael Pankov

Respostas:

4

Então, qual é a razão para o Padrão C considerar a recursividade constante, como ela diz?

Apenas de uma perspectiva de tipo, não fazê-lo seria doentio (em outras palavras: terrivelmente quebrado e intencionalmente não confiável).

E isso é por causa do que "=" significa em uma estrutura: é uma atribuição recursiva. Segue-se que, eventualmente, você está s1._a = <value>acontecendo "dentro das regras de digitação". Se o padrão permitir isso para constcampos "aninhados" , isso adicionará uma inconsistência séria em sua definição de sistema de tipos como uma contradição explícita (pode muito bem jogar o constrecurso fora, pois ele se tornou inútil e não confiável por sua própria definição).

Sua solução (1), pelo que entendi, está forçando desnecessariamente toda a estrutura a estar constsempre que um de seus campos estiver const. Dessa forma, s1._b = bseria ilegal para um ._bcampo não const em um não const que s1contenha a const a.

Thiago Silva
fonte
Bem. Cmal tem um sistema de som (mais como um monte de caixas de canto amarradas umas às outras ao longo dos anos). Além disso, a outra maneira de atribuir a tarefa a structé memcpy(s_dest, s_src, sizeof(S_t)). E tenho certeza de que é a maneira real como é implementada. E nesse caso, mesmo o "sistema de tipos" existente não o proíbe.
Michael Pankov
2
Muito verdadeiro. Espero não ter sugerido que o sistema de tipos de C seja bom, apenas que deliberadamente tornar uma semântica específica doentia o derrote deliberadamente. Além disso, embora o sistema de tipos de C não seja fortemente imposto, as maneiras de violá-lo são muitas vezes explícitas (ponteiros, acesso indireto, projeções) - mesmo que seus efeitos sejam frequentemente implícitos e difíceis de rastrear. Assim, ter "cercas" explícitas para violá-las informa melhor do que ter uma contradição nas próprias definições.
Thiago Silva
2

O motivo é que os campos somente leitura são somente leitura. Não há grande surpresa lá.

Você está assumindo erroneamente que o único efeito é o posicionamento na ROM, o que é realmente impossível quando existem campos não-const adjacentes. Na realidade, os otimizadores podem assumir que as constexpressões não foram gravadas e otimizar com base nisso. É claro que essa suposição não se aplica quando existem pseudônimos não constantes.

Sua solução (1) quebra o código legal e razoável existente. Isso não vai acontecer. Sua solução (2) praticamente remove o significado de constmembros. Enquanto isso não quebra o código existente, parece não ter uma justificativa.

MSalters
fonte
Tenho 90% de certeza que os otimizadores podem não supor que os constcampos não sejam gravados, porque você sempre pode usar memsetor memcpy, e isso seria compatível com o Padrão. (1) pode ser implementado como, no mínimo, aviso adicional, ativado por uma bandeira. (2) a lógica é que, bem, exatamente - não há como um componente structser considerado não gravável quando toda a estrutura é gravável.
Michael Pankov
Um "diagnóstico opcional determinado por uma bandeira" seria um requisito exclusivo para a Norma exigir. Além disso, definir a flag ainda quebraria o código existente, de modo que ninguém se incomodaria com a flag e seria um beco sem saída. Quanto a (2), 6.3.2.1:1 especifica exatamente o oposto: a estrutura inteira não é gravável sempre que um componente é. No entanto, outros componentes ainda podem ser graváveis. Cf. C ++ que define também operator=em termos de membros e, portanto, não define a operator=quando um membro é const. C e C ++ ainda são compatíveis aqui.
MSalters
@constantius - O fato de que você PODE fazer algo para contornar deliberadamente a constância de um membro NÃO é um motivo para o otimizador ignorar essa constância. Você pode eliminar a constância dentro de uma função, permitindo alterar as coisas. Mas o otimizador no contexto de chamada ainda pode assumir que você não o fará. O Constness é útil para o programador, mas, em alguns casos, também é um convite para o otimizador.
Michael Kohne
Então, por que uma estrutura não gravável pode ser substituída por ie memcpy? Por outras razões - ok, é legado, mas por que foi feito dessa maneira em primeiro lugar?
Michael Pankov
11
Ainda estou imaginando se seu comentário memcpyestá correto. AFACIT A citação de John Bode em sua outra pergunta está certa: seu código grava em um objeto qualificado como const e, portanto, NÃO é uma reclamação padrão, final da discussão.
MSalters