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?


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.


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.


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.