Qual è la ragione per cui lo standard C considera la costanza ricorsivamente?


9

Lo standard C99 dice in 6.5.16: 2:

Un operatore di assegnazione deve avere un valore modificabile come operando di sinistra.

e in 6.3.2.1:1:

Un lvalue modificabile è un lvalue che non ha un tipo di array, non ha un tipo incompleto, non ha un tipo con qualifica costante e se è una struttura o unione, non ha alcun membro (incluso, ricorsivamente, qualsiasi membro o elemento di tutti gli aggregati o sindacati contenuti) con un tipo con qualifica const.

Consideriamo ora un non const structcon un constcampo.

typedef struct S_s {
    const int _a;
} S_t;

Per impostazione predefinita, il codice seguente è un comportamento indefinito (UB):

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

Il problema semantico con questo è che l'entità che racchiude ( struct) dovrebbe essere considerata scrivibile (non di sola lettura), a giudicare dal tipo dichiarato dell'entità ( S_t s1), ma non dovrebbe essere considerata scrivibile dalla formulazione di standard (le 2 clausole in alto) a causa del constcampo _a. Lo standard rende poco chiaro per un programmatore che legge il codice che l'assegnazione è in realtà un UB, perché è impossibile dirlo senza la definizione di struct S_s ... S_ttipo.

Inoltre, l'accesso in sola lettura al campo viene comunque applicato solo sintatticamente. Non è possibile che alcuni constcampi di non- const structdavvero vengano collocati in un archivio di sola lettura. Ma una tale formulazione di standard mette al bando il codice che elimina deliberatamente il constqualificatore di campi nelle procedure accessorie di questi campi, in questo modo ( è una buona idea const-qualificare i campi della struttura in 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);
}

Quindi, per qualche motivo, per un intero structessere di sola lettura è sufficiente dichiararloconst

const S_t s3;

Ma per un intero structnon essere di sola lettura non è sufficiente dichiararlo senza const.

Quello che penso sarebbe meglio, è:

  1. Vincolare la creazione di non conststrutture con constcampi ed emettere una diagnostica in tal caso. Ciò chiarirebbe che i structcampi di sola lettura contenenti sono di sola lettura.
  2. Definire il comportamento in caso di scrittura su un constcampo appartenente a una non conststruttura in modo da rendere il codice sopra (*) conforme allo Standard.

Altrimenti il ​​comportamento non è coerente e difficile da capire.

Quindi, qual è la ragione per cui C Standard considera constricorsivamente -ness, come dice?


Ad essere sincero, non vedo una domanda lì dentro.
Bart van Ingen Schenau,

@BartvanIngenSchenau modificato per aggiungere la domanda indicata nell'argomento alla fine del corpo
Michael Pankov,

1
Perché il downvote?
Michael Pankov,

Risposte:


4

Quindi, qual è la ragione per cui C Standard considera la costanza ricorsivamente, come dice?

Dal solo punto di vista del tipo, non farlo sarebbe insensato (in altre parole: terribilmente rotto e intenzionalmente inaffidabile).

E questo a causa di ciò che "=" significa su una struttura: è un incarico ricorsivo. Ne consegue che alla fine si s1._a = <value>verifica un "dentro le regole di battitura". Se lo standard lo consente per i constcampi "nidificati" , l'aggiunta di una grave incoerenza nella definizione del sistema di tipi come una contraddizione esplicita (potrebbe anche gettare via la constfunzione, in quanto è diventata inutile e inaffidabile per definizione).

La tua soluzione (1), per quanto ho capito, sta costringendo inutilmente l'intera struttura ad essere constogni volta che uno dei suoi campi è const. In questo modo, s1._b = bsarebbe illegale per un ._bcampo non const su un non const s1contenente a const a.


Bene. Cha a malapena un sistema di tipi di suono (più simile a un mucchio di custodie angolari legate l'una sull'altra nel corso degli anni). Inoltre, l'altro modo di assegnare l'incarico a structè memcpy(s_dest, s_src, sizeof(S_t)). E sono abbastanza sicuro che sia implementato nel modo reale. E in tal caso anche il "sistema di tipi" esistente non ti proibisce di farlo.
Michael Pankov,

2
Verissimo. Spero di non sottintendere che il sistema di tipo C sia solido, ma solo che deliberatamente una semantica non corretta deliberatamente la sconfigge. Inoltre, sebbene il sistema di tipo C non sia fortemente applicato, i modi per violarlo sono spesso espliciti (puntatori, accesso indiretto, cast) - anche se i suoi effetti sono spesso impliciti e difficili da rintracciare. Pertanto, avere esplicite "recinzioni" per violarle informa meglio che avere una contraddizione nelle definizioni stesse.
Thiago Silva,

2

Il motivo è che i campi di sola lettura sono di sola lettura. Nessuna grande sorpresa lì.

Stai erroneamente supponendo che l'unico effetto sia sul posizionamento nella ROM, il che è effettivamente impossibile quando ci sono campi non costanti adiacenti. In realtà, gli ottimizzatori possono presumere che le constespressioni non siano scritte e ottimizzare in base a ciò. Ovviamente tale presupposto non vale quando esistono alias non costanti.

La tua soluzione (1) rompe il codice legale e ragionevole esistente. Ciò non accadrà. La tua soluzione (2) rimuove praticamente il significato dei constmembri. Anche se questo non romperà il codice esistente, sembra mancare di una logica.


Sono sicuro al 90% che gli ottimizzatori non possono presumere che i constcampi non siano scritti, perché puoi sempre usare memseto memcpy, e che sarebbe persino conforme allo standard. (1) può essere implementato come almeno un avviso aggiuntivo, abilitato da un flag. La logica di (2) è che, beh, esattamente - non c'è modo in cui un componente structpossa essere considerato non scrivibile quando l'intera struttura è scrivibile.
Michael Pankov,

Una "diagnostica facoltativa determinata da una bandiera" sarebbe un requisito univoco per lo standard da richiedere. Inoltre, impostare la bandiera romperebbe ancora il codice esistente, quindi in effetti nessuno si preoccuperebbe della bandiera e sarebbe un vicolo cieco. Per quanto riguarda (2), 6.3.2.1:1 specifica esattamente il contrario: l'intera struttura non è scrivibile ogni volta che un componente lo è. Tuttavia, altri componenti potrebbero essere comunque scrivibili. Cf. C ++ che definisce anche operator=in termini di membri e quindi non definisce un operator=quando lo è un membro const. C e C ++ sono ancora compatibili qui.
MSalters il

@constantius - Il fatto che PUOI fare qualcosa per aggirare deliberatamente la costanza di un membro NON è un motivo per cui l'ottimizzatore ignori quella costanza. Puoi eliminare la costanza all'interno di una funzione, permettendoti di cambiare roba. Ma l'ottimizzatore nel contesto chiamante può ancora supporre che non lo farai. Constness è utile per il programmatore, ma in alcuni casi è anche un god-send per l'ottimizzatore.
Michael Kohne,

Allora perché una struttura non scrivibile può essere sovrascritta con ie memcpy? Come per altri motivi - ok, è eredità, ma perché è stato fatto in questo modo in primo luogo?
Michael Pankov,

1
Mi chiedo ancora se il tuo commento su memcpyè giusto. AFACIT La citazione di John Bode nell'altra tua domanda è giusta: il tuo codice scrive su un oggetto con qualifica const e quindi NON è un reclamo standard, fine della discussione.
MSalters il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.