Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?

Unie i niezdefiniowane zachowanie w C++17

Ostatnio zmodyfikowano 2018-03-15 20:21
Autor Wiadomość
luks
Temat założony przez niniejszego użytkownika
» 2018-03-13 14:56:18
Wczoraj Stroustrup odpisał:

"Yes. t.b.second = 2; cout << t.a.first << '\n'; You write as one union member and read as another. That's UB."

Wygląda to jednak na powierzchowne potraktowanie tematu, bo nie odniósł się np. do common initial sequence. Wysłałem także e-mail do Michała Dominiaka, który jest delegatem do JTC1/SC22/WG21, ale nie uzyskałem na razie odpowiedzi. Testowałem kod na GCC, Clang oraz Visual C++ i wszędzie są takie same wyniki. Z zebranych materiałów wynika raczej, że ten kod jest poprawny. Martwi mnie jedynie to, że przykłady, które znalazłem, nadają wartość całej strukturze. Nie jest podawane, czy można inicjować struktury w połowie. Gdy ustawiamy pierwsze pole w pierwszej strukturze, a następnie ustawiamy drugie pole w drugiej strukturze, to co dzieje się z danymi z pierwszego pola? Kompilator w takim wypadku automatycznie zmienia aktywne pole unii, ale czy standard gwarantuje, że dane z pierwszego pola zostaną na swoim miejscu? Chyba pozostaje mocno obłożyć kod testami sprawdzającymi zachowanie kompilatora...
P-169987
jankowalski25
» 2018-03-13 18:31:31
O, nie spodziewałem się, że taki kod może być uznawany za poprawny. Zastanawia mnie w takim razie na ile "podobne" muszą być poszczególne pola, aby można było tak zrobić? Na przykład czy kod, w którym jedna składowa będzie publiczna, a druga prywatna może spowodować, że w ten sposób będzie się dało omijać specyfikatory dostępu? Bo na razie, zgodnie z tym, co kiedyś pisał Elaine, da się to zrobić (zgodnie ze standardem) za pomocą szablonów. Albo czy typy danych muszą być całkowicie zgodne, czy wystarczy ogólnie rozumiana zgodność? Jeśli mamy klasę
Integer
 z jednym polem typu
int
, to czy możemy jej użyć zamiennie z tym typem (pod warunkiem, że zajmuje ten sam rozmiar oraz dostarcza odpowiednie operatory, dzięki czemu może zostać użyta 1:1 w tym samym kontekście)? Albo jeszcze ciekawiej, gdy pola się częściowo pokrywają, ale po połączeniu unią są identyczne:
C/C++
struct A
{
    char c;
    int i;
};
struct B
{
    double d;
};
struct C
{
    char c;
};
struct D
{
    int i;
    double d;
};
struct E
{
    union
    {
        A a;
        B b;
    };
};
struct F
{
    union
    {
        C c;
        D d;
    };
};
struct G
{
    union
    {
        E e;
        F f;
    };
};

Chyba pozostaje mocno obłożyć kod testami sprawdzającymi zachowanie kompilatora...
Albo zaimplementować to ręcznie korzystając z tego, co na pewno wszędzie zadziała. Wtedy nawet jak testy pokażą, że jakiś kompilator na to nie pozwala, to będzie można skorzystać z własnej implementacji, która zapewni to, co trzeba. Chyba że to się odbije znacząco na wydajności, ale to już osobny problem. Być może szablony wystarczą, aby to załatwić w czasie kompilacji.
P-169989
luks
Temat założony przez niniejszego użytkownika
» 2018-03-13 19:19:55
Nie orientuję się, jak sprawa wygląda przy dalszym zagnieżdżaniu tego wszystkiego.

Struktury/klasy w unii muszą być StandardLayoutType, aby posiadały common initial sequence. Można o tym poczytać w http://en.cppreference.com/w​/cpp/language/data_members (sekcja Standard layout).
Mamy tam: "Two standard-layout non-union class types may have a common initial sequence of non-static data members and bit-fields (since C++14), for a sequence of one or more initial members (in order of declaration), if the members have layout-compatible types and if they are bit-fields, they have the same width (since C++14)." W dokumencie http://www.open-std.org/jtc1​/sc22/wg21/docs/papers/2018​/n4727.pdf (strona 67, punkt 11) jest: "Two types cv1 T1 and cv2 T2 are layout-compatible types if T1 and T2 are the same type, layout-compatible enumerations (10.2), or layout-compatible standard-layout class types (12.2)."

I teraz właśnie nie jestem pewien, czy inicjacja struktury w unii musi być pełna, czy nie. Bo jeżeli nie musi być pełna, to technikę można wykorzystać np. do implementacji swizzling w klasach Vector2, Vector3, Vector4, Matrix2...
P-169992
garlonicon
» 2018-03-13 19:45:39
Chociaż z drugiej strony jeśli jedynym problemem jest zarządzanie pamięcią, czyli co gdzie włożyć i w jakiej kolejności, to może lepiej skorzystać z placement new zamiast kombinować z uniami? A sama pamięć nie musi być alokowana na stosie stercie (ang. heap), skoro operatory
new
 i
delete
 można przeciążać i zaimplementować je dowolnie. Chyba nawet gdyby tam były jakieś wstawki asma pakujące dane w rejestry, to raczej nie powinno być większych problemów (chociaż wtedy zamiast wstawek lepiej napisać bibliotekę w asmie i połączyć ją linkerem z kodem C++).
P-169993
pekfos
» 2018-03-13 20:17:31
No tak, podałem kod i pytam, czy jest poprawny. Szczyt bezczelności. A co byś chciał? Mam ci tutaj wkleić 30 nagłówków i napisać opis dziedzinowy na 30 stron? Kolego, kod jest przejrzysty, zawiera meritum, więc nie mydl mi oczu formatowaniem. Jeżeli uważasz, że pytania dotyczące UB są bezużyteczne, to nie mamy o czym rozmawiać. Poczytaj sobie np. o UBSan. Korporacja Google nie utworzyłaby takiego narzędzia, gdyby UB było sprawą marginalną.
To nie szczyt bezczelności, ale powoli się na niego wspinasz. "Poruszam tu istotny problem! Pal licho zasady i jasne opisanie problemu - domyślą się o co mi chodzi. Co?! Temat bezużyteczny?! Żądam satysfakcji!" Nie twierdzę, że UB to nie jest istotny problem - wręcz przeciwnie, ale to nie znaczy że pytania o to mają bezwarunkowo tą istotność odziedziczyć. Gdzie twoim zdaniem będzie użyteczność takiego tematu za miesiąc, rok? Ktoś szukając odpowiedzi na podobne pytanie zapyta wujka Googla, czy jego kod jest poprawny..?
P-169994
luks
Temat założony przez niniejszego użytkownika
» 2018-03-13 20:24:57
Kolego, widzę, że masz jakieś problemy ze sobą. I nie chodzi tu tylko o ten wątek, wystarczy przeglądnąć twoje posty. Tematu bardziej jasno nie da się opisać, mogę go najwyżej zaciemnić dodając zalecane przez Ciebie bzdury. Zostaw ten temat, napij się ziółek i wróć do swoich zajęć. Rozmawiamy tu o poważnych sprawach, więc nie mam czasu na twoje pieniactwo.
P-169995
pekfos
» 2018-03-13 21:59:02
Niech będzie, porozmawiajmy o poważnych sprawach.
In a union, a non-static data member is active if its name refers to an object whose lifetime has begun and
has not ended (6.6.3). At most one of the non-static data members of an object of union type can be active
at any time, that is, the value of at most one of the non-static data members can be stored in a union at any
time. [ Note: One special guarantee is made in order to simplify the use of unions: If a standard-layout union
contains several standard-layout structs that share a common initial sequence (12.2), and if a non-static data
member of an object of this standard-layout union type is active and is one of the standard-layout structs, it
is permitted to inspect the common initial sequence of any of the standard-layout struct members; see 12.2.
—end note ]
12.3, Unie. Weźmy teraz pierwotny kod
C/C++
Test t; // 0
t.a.first = 1; // 1
t.b.second = 2; // 2
Ustawiane są kolejne składowe (w pamięci) unii, ale przez jej różne właściwe składowe. Tylko jedna ze składowych unii może być w danym momencie aktywna. W momencie 0, żadna nie jest aktywna. Pierwsze przypisanie tworzy t.a (zgodnie z 12.3.5) - t.a.first ma przypisane 1, t.a.second jest niezainicjalizowane. Przypisanie do t.a.second byłoby nudne, więc przypisujemy do t.b.second. t.b jest nieaktywne, więc to nie jest żyjący obiekt.
if modification of X would have undefined
behavior under 6.6.3, an object of the type of X is implicitly created in the nominated storage; no initialization
is performed and the beginning of its lifetime is sequenced after the value computation of the left and right
operands and before the assignment. [ Note: This ends the lifetime of the previously-active member of the
union, if any
(6.6.3). —end note ]
t.a.first jest w tym momencie niszczone, bo kończy się czas życia obiektu t.a. t.b jest utworzone, t.b.first jest niezainicjalizowane. To jest element common initial sequence, więc to jest ta sama lokacja pamięci, więc w zasadzie wartość tam dalej powinna być, ale o ile nie ma na to gdzieś reguły, to jest to UB. Gdy t.a żyło, można było odczytać t.b.first przez wyjątek na common initial sequence, ale standard mówi tylko o odczycie (inspect). Gdyby zapisać coś do t.b.first, zmieniłoby to aktywny obiekt.

Chyba pozostaje mocno obłożyć kod testami sprawdzającymi zachowanie kompilatora...
Biorąc pod uwagę naturę UB, mogłoby to być dość trudne, jeśli nie niemożliwe. Musiałbyś zmusić kompilator do wykorzystania UB. Jeśli ci się uda, to brawo - nie powinieneś pisać takiego kodu, bo to UB. Jeśli się nie uda, to zawsze jest ryzyko, że nie starałeś się dostatecznie bardzo, lub po prostu testowany kompilator nie wykorzystuje tego konkretnego UB w optymalizacjach.
P-169997
luks
Temat założony przez niniejszego użytkownika
» 2018-03-13 22:57:18
"...ale o ile nie ma na to gdzieś reguły, to jest to UB..." -> Gratuluję odkrycia. Ja staram się właśnie od wczoraj ustalić, czy jest taka reguła, czy jej nie ma. Cieszę się, że jesteśmy już na tej samej stronie.
P-169998
1 « 2 » 3 4
Poprzednia strona Strona 2 z 4 Następna strona