Jaki jest powód, dla którego standard C rozważa rekurencyjność?

9

Norma C99 mówi w 6.5.16: 2:

Operator przypisania ma modyfikowalną wartość jako lewy operand.

oraz w 6.3.2.1:1:

Zmienna wartość jest wartością, która nie ma typu tablicowego, nie ma typu niekompletnego, nie ma typu stałego kwalifikowania, a jeśli jest strukturą lub związkiem, nie ma żadnego elementu (w tym, rekurencyjnie, żadnego elementu lub element wszystkich zawartych agregatów lub związków) o typie const.

Teraz rozważmy nie- const structz constpola.

typedef struct S_s {
    const int _a;
} S_t;

Standardowo następujący kod jest niezdefiniowanym zachowaniem (UB):

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

Problem semantyczny polega na tym, że encja zamykająca ( struct) powinna być uważana za zapisywalną (nie tylko do odczytu), sądząc po zadeklarowanym typie encji ( S_t s1), ale nie powinna być uważana za zapisywalną na podstawie sformułowania standardowego (2 klauzule na górze) z powodu constpola _a. Standard sprawia, że ​​dla programisty czytającego kod nie jest jasne, że przypisanie jest w rzeczywistości UB, ponieważ nie można powiedzieć, że bez definicji struct S_s ... S_ttypu.

Co więcej, dostęp tylko do odczytu do pola jest i tak egzekwowany tylko syntaktycznie. Nie ma możliwości, aby niektóre constpola nie- const structtak naprawdę zostały umieszczone w pamięci tylko do odczytu. Ale takie sformułowanie standardu zakazuje kodu, który celowo odrzuca constkwalifikator pól w procedurach akcesyjnych tych pól, podobnie jak ( Czy dobrym pomysłem jest stałe kwalifikowanie pól struktury w 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);
}

Z jakiegoś powodu, aby całość structbyła tylko do odczytu, wystarczy ją zadeklarowaćconst

const S_t s3;

Ale aby całość structnie była tylko do odczytu, nie wystarczy zadeklarować jej brak const.

To, co moim zdaniem byłoby lepsze, to:

  1. Ograniczanie tworzenia niestruktur za constpomocą constpól i w takim przypadku wydawanie diagnozy. To by wyjaśniało, że structzawierające pola tylko do odczytu jest tylko do odczytu.
  2. Zdefiniowanie zachowania w przypadku zapisu w constpolu należącym do conststruktury innej niż strukturalna w celu dostosowania powyższego kodu (*) do standardu.

W przeciwnym razie zachowanie nie będzie spójne i trudne do zrozumienia.

Jaki jest zatem powód, dla którego C Standard rozważa constrekurencyjność, jak to ujmuje?

Michael Pankov
źródło
Szczerze mówiąc, nie widzę w tym pytania.
Bart van Ingen Schenau,
@BartvanIngenSchenau zredagowano, aby dodać pytanie zawarte w temacie na końcu ciała
Michael Pankov
1
Dlaczego głosowanie negatywne?
Michael Pankov,

Odpowiedzi:

4

Jaki jest zatem powód, dla którego C Standard rozważa rekurencyjność, jak to ujmuje?

Z samej perspektywy typowej, niezrobienie tego byłoby niewłaściwe (innymi słowy: strasznie złamane i celowo niewiarygodne).

A to dlatego, że „=” oznacza strukturę: jest to zadanie rekurencyjne. Wynika z tego, że w końcu s1._a = <value>zdarza się „wewnątrz zasad pisania”. Jeśli norma zezwala na to w przypadku constpól „zagnieżdżonych” , dodanie poważnej niespójności w definicji systemu typów jako wyraźnej sprzeczności (może równie dobrze odrzucić tę constfunkcję, ponieważ stała się bezużyteczna i zawodna z samej definicji).

Wasze rozwiązanie (1), o ile rozumiem, niepotrzebnie zmusza całą strukturę do działania, constilekroć znajduje się jedno z jej pól const. W ten sposób s1._b = bbyłoby niezgodne z prawem dla ._bpola niestałego na niestałym s1zawierającym const a.

Thiago Silva
źródło
Dobrze. Cledwo ma system dźwiękowy (bardziej jak wiązka narożnych skrzynek przymocowanych do siebie na przestrzeni lat). Poza tym innym sposobem przypisania przypisania do a structjest memcpy(s_dest, s_src, sizeof(S_t)). I jestem prawie pewien, że jest to faktyczny sposób implementacji. W takim przypadku nawet istniejący „system typów” nie zabrania tego.
Michael Pankov,
2
Bardzo prawdziwe. Mam nadzieję, że nie zasugerowałem, że system typu C jest dobry, tylko że umyślnie czyni konkretną semantykę niesłusznie celowo go pokonując. Ponadto, chociaż system typów C nie jest silnie egzekwowany, sposoby jego złamania są często jawne (wskaźniki, pośredni dostęp, rzutowania) - nawet jeśli jego skutki są często niejawne i trudne do śledzenia. Zatem posiadanie wyraźnych „ogrodzeń”, aby je złamać, informuje lepiej niż sprzeczność w samych definicjach.
Thiago Silva,
2

Powodem jest to, że pola tylko do odczytu są tylko do odczytu. Nie ma w tym żadnej wielkiej niespodzianki.

Błędnie zakładasz, że jedynym efektem jest umieszczenie w pamięci ROM, co w istocie jest niemożliwe, gdy istnieją sąsiednie pola nie stałe. W rzeczywistości optymalizatorzy mogą zakładać, że constwyrażenia nie są zapisywane, i na ich podstawie optymalizować. Oczywiście założenie to nie obowiązuje, gdy istnieją nie-aliasy.

Twoje rozwiązanie (1) łamie obowiązujący kodeks prawny i rozsądny. To się nie stanie. Twoje rozwiązanie (2) praktycznie usuwa znaczenie constczłonków. Chociaż nie zepsuje to istniejącego kodu, wydaje się, że nie ma uzasadnienia.

MSalters
źródło
Jestem w 90% pewien, że optymalizatorzy mogą nie założyć, że constpola nie są zapisane, ponieważ zawsze można użyć memsetlub memcpy, a nawet byłoby to zgodne ze standardem. (1) można zaimplementować jako co najmniej dodatkowe ostrzeżenie, włączane przez flagę. Uzasadnieniem (2) jest to, cóż, dokładnie - nie ma mowy, aby element składowy structmógł zostać uznany za niemożliwy do zapisu, gdy cała struktura jest zapisywalna.
Michael Pankov,
„Opcjonalna diagnostyka określona przez flagę” byłaby unikalnym wymaganiem dla standardu. Poza tym ustawienie flagi nadal łamałoby istniejący kod, więc w efekcie nikt nie zawracałby sobie głowy flagą i byłby to ślepy zaułek. Jeśli chodzi o (2), 6.3.2.1:1 określa dokładnie odwrotnie: w całej strukturze nie można zapisywać, ilekroć jest jeden komponent. Jednak inne komponenty mogą nadal nadawać się do zapisu. Por. C ++, który definiuje również operator=członków, a zatem nie określa, operator=kiedy jest jednym członkiem const. C i C ++ są tutaj nadal kompatybilne.
MSalters
@constantius - Fakt, że MOŻESZ zrobić coś, aby celowo ominąć stałość członka, NIE jest powodem, dla którego optymalizator zignoruje tę stałość. Możesz odrzucić constness wewnątrz funkcji, pozwalając ci zmieniać rzeczy. Ale optymalizator w kontekście wywoływania nadal może zakładać, że nie. Ciągłość jest przydatna dla programisty, ale w niektórych przypadkach jest również godnym posłaniem dla optymalizatora.
Michael Kohne,
Dlaczego więc można nadpisać strukturę, której nie można zapisać, np. memcpy? Jeśli chodzi o inne powody - ok, to jest dziedzictwo, ale dlaczego tak się stało?
Michael Pankov,
1
Nadal zastanawiam się, czy twój komentarz memcpyjest słuszny. UWAŻAJ, że cytat Johna Bode'a w twoim drugim pytaniu jest słuszny: twój kod zapisuje obiekt o stałym parametrze i dlatego NIE jest standardową skargą, zakończeniem dyskusji.
MSalters