Forum: PC-Programmierung (Wann) Ist das (nicht) UB ?


von NaNi (Gast)


Lesenswert?

Guten Abend.
Ja, das ist schon die ganze Frage: (Wann) Ist das (nicht) UB ?
...eine der Methoden aufzurufen?

...Und unter welchen Umständen wird es trotzdem relativ gefahrlos 
gemacht?

...Und warum spricht -fsanitize=undefined nicht darauf an? Ich dachte, 
dazu sei das da!
1
template<typename T>
2
struct A{
3
  T e0;
4
  T e1;
5
  T e2;
6
  //...
7
};
8
9
template<typename T>
10
struct B{
11
  T e0;
12
  T e1;
13
  T e2;
14
  //...
15
16
  //Wann kann man dies aufrufen?
17
  T & operator[](int i){
18
    return (*reinterpret_cast<const T(*)[3]>(this))[i];
19
  }
20
  //Oder dies
21
  A<T> & asA(int i){
22
    return (*reinterpret_cast<A<T>*>(this));
23
  }
24
};

Danke schon mal.

MfG

von MaWin (Gast)


Lesenswert?

Einen reinterpret_cast kann man grob gesagt immer dann gefahrlos machen, 
wenn das Objekt tatsächlich dem entspricht, in das man es hincastet.

Fast immer gibts aber bessere Wege als reinterpret_cast.

von NaNi (Gast)


Lesenswert?

Und wie kommt man in dem Fall dann vorher an etwas, das nicht dem Objekt 
entspricht, ohne UB?

Zu dem Fall oben: Warum spricht -fsanitize=undefined nicht darauf an? 
Ich dachte,
dazu sei das da!
Man könnte evtl sagen, dass das was da ist, zufällig dem entspricht, was 
es sein soll. Kann man das Bsp dann so verändern, dass es nicht mehr 
geht?

von Rolf M. (rmagnus)


Lesenswert?

NaNi schrieb:
> (Wann) Ist das (nicht) UB ?
> ...eine der Methoden aufzurufen?

Ich würde sagen, eine struct als Array umzuinterpretieren und dann per 
Index zuzugreifen ist immer UB. Die zweite Funktion ist soweit ich 
verstehe auch immer UB, da man mit dem Zeiger, der aus dem 
reintepret_cast rausfällt, nicht viel mehr machen darf, als ihn wieder 
in den ursprünglichen Typ zurückzucasten.
Wobei es lustigerweise erlaubt wäre, über eine union zu gehen, sofern T 
ein POD ist. Wenn eine union zwei structs enthält, die POD sind und 
gleich anfangen, dann ist es erlaubt, auf die entsprechenden Member auch 
über die "falsche" Struktur zuzugreifen.

von MaWin (Gast)


Lesenswert?

NaNi schrieb:
> Warum spricht -fsanitize=undefined nicht darauf an?

Weil das natürlich nicht alle Arten von UB abfangen kann.

> Und wie kommt man in dem Fall dann vorher an etwas, das nicht dem Objekt
entspricht, ohne UB?

Hä?

von MaWin (Gast)


Lesenswert?

Hast du einmal versucht das vernünftig mit einem switch zu 
implementieren?
Welcher Code wird daraus optimiert? Der Compiler sollte in der Lage sein 
zu erkennen, dass die Elemente in einer Reihe liegen und dann per 
pointer arithmetik zugreifen können (nach einer initialen 
Bereichsabfrage).

von NaNi (Gast)


Lesenswert?

Rolf M. schrieb:
> Wobei es lustigerweise erlaubt wäre, über eine union zu gehen, sofern T
> ein POD ist. Wenn eine union zwei structs enthält, die POD sind und
> gleich anfangen, dann ist es erlaubt, auf die entsprechenden Member auch
> über die "falsche" Struktur zuzugreifen.

Moment, wie?! Soweit ich weiß, existiert immer nur ein Member eines 
Union gleichzeitig.
Wenn sie jetzt ein komplett, statt nur zu Beginn, identisches 
Datenlayout hätten, ok, aber was meinst du mit "gleich anfangen"? 
Padding z.B. gibts doch auch bei POD.

MaWin schrieb:
> Weil das natürlich nicht alle Arten von UB abfangen kann.

Weißt du, ob das die Bedründung ist? Ich meine, könnte sein, aber muss 
das der Grund sein? Es scheint Ausnahmen zu geben, dort, wo der Compiler 
mit Absicht nicht standardkonform (aber irgendwie noch ISO-xy-konform) 
ist.

MaWin schrieb:
> Hä?

Ah, schon gut. Denkfehler.

MaWin schrieb:
> Hast du einmal versucht das vernünftig mit einem switch zu
> implementieren?
> Welcher Code wird daraus optimiert? Der Compiler sollte in der Lage sein
> zu erkennen, dass die Elemente in einer Reihe liegen und dann per
> pointer arithmetik zugreifen können (nach einer initialen
> Bereichsabfrage).

Das wollte ich mir eigentlich ersparen. Wenn schon Array, dann auch mit 
theoretisch 50k Einträgen..

von MaWin (Gast)


Lesenswert?

NaNi schrieb:
> Wenn schon Array, dann auch mit
> theoretisch 50k Einträgen..

Das kannst du ja machen. Aber dann solltest du es auch komplett 
durchziehen und auf das struct ganz verzichten.

von NaNi (Gast)


Lesenswert?

MaWin schrieb:
> Das kannst du ja machen. Aber dann solltest du es auch komplett
> durchziehen und auf das struct ganz verzichten.

Nein.
Ich meinte damit, ich würde mich dabei weniger auf den Compiler 
verlassen.
"Sollte erkennen" == UB ...

von MaWin (Gast)


Lesenswert?

NaNi schrieb:
> "Sollte erkennen" == UB ...

Unsinn.

von NaNi (Gast)


Lesenswert?

MaWin schrieb:
> Unsinn.

Aber auf "Sollte erkennen" == Konjunktiv ... können wir uns einigen?

MaWin schrieb:
> Das kannst du ja machen. Aber dann solltest du es auch komplett
> durchziehen und auf das struct ganz verzichten.

Wozu dann noch:

MaWin schrieb:
> Hast du einmal versucht das vernünftig mit einem switch zu
> implementieren?

??

von MaWin (Gast)


Lesenswert?

NaNi schrieb:
> Wozu dann noch:

Ja. Entweder, oder.

Jedenfalls ist keines davon UB. Im Gegensatz zu dem reinterpret_cast.

von Zombie (Gast)


Lesenswert?

1
   T & operator[](int i){
2
     return (*reinterpret_cast<const T(*)[3]>(this))[i];
3
   }

ich glaube, das ist kein UB. Warum? Siehe hier:
https://en.cppreference.com/w/cpp/numeric/complex (siehe unter 
Array-oriented access)

Ich kann mir kaum vorstellen, dass für std::complex hier spezielle 
regeln gelten.

von Rolf M. (rmagnus)


Lesenswert?

NaNi schrieb:
> Rolf M. schrieb:
>> Wobei es lustigerweise erlaubt wäre, über eine union zu gehen, sofern T
>> ein POD ist. Wenn eine union zwei structs enthält, die POD sind und
>> gleich anfangen, dann ist es erlaubt, auf die entsprechenden Member auch
>> über die "falsche" Struktur zuzugreifen.
>
> Moment, wie?! Soweit ich weiß, existiert immer nur ein Member eines
> Union gleichzeitig.

Im Grunde ja, aber es gibt die von mir beschriebene Ausnahme, die von C 
übernommen wurde. Hauptsächlich wohl, um in etwa so was zu ermöglichen:
1
enum event_type
2
{
3
    mouse_move, mouse_click
4
};
5
6
struct event
7
{
8
    event_type type;
9
};
10
11
struct move_event
12
{
13
    event_type type;
14
    int x, y;
15
};
16
17
struct click_event
18
{
19
    event_type type;
20
    int button;
21
};
22
23
union event_union
24
{
25
    event event;
26
    move_event move;
27
    click_event click;
28
};
29
30
void handle_event(const event_union* u)
31
{
32
    switch (u->event.type) // Zugriff über anderes Element
33
    {
34
        case mouse_move:
35
            // handle u.move
36
            break;
37
        case mouse_click:
38
            // handle u.click
39
            break;
40
}

> Wenn sie jetzt ein komplett, statt nur zu Beginn, identisches
> Datenlayout hätten, ok, aber was meinst du mit "gleich anfangen"?

Ich meine, dass die ersten paar Member in den Strukturen von Typen sind, 
die das gleiche Layout haben (in C++-Sprech "common initial sequence"). 
Dann hat auch der Teil der Strukturen das gleiche Layout (in C++-Sprech 
"layout-compatible"), und man darf auf genau diese auch über die andere 
Struktur zugreifen. Hier der Original-Text aus dem C++17-Draft:
"If a standard-layout union contains two or more standard-layout structs 
that share a common initial sequence, and if the standard-layout union 
object currently contains one of these standard-layout structs, it is 
permitted to inspect the common initial part of any of them. Two 
standard-layout structs share a common initial sequence if corresponding 
members have layout-compatible types and either neither member is a 
bit-field or
both are bit-fields with the same width for a sequence of one or more 
initial members."

> Padding z.B. gibts doch auch bei POD.

Ja, aber wenn die Member die gleichen sind, ist auch das Padding das 
gleiche.

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Als Lösung dafür gibt es std::bit_cast (ist auch constexpr).

Beitrag #7225118 wurde vom Autor gelöscht.
von NaNi (Gast)


Lesenswert?

Rolf M. schrieb:
> Im Grunde ja, aber es gibt die von mir beschriebene Ausnahme, die von C
> übernommen wurde. Hauptsächlich wohl, um in etwa so was zu ermöglichen:

Ah, ja. Jetzt verstehe ich was du meinst. Anscheinend gab es vor c11 
keine anonymen unions. Was diese Ausnahme vieleicht nötig gemacht hat.

Wilhelm M. schrieb:
> Als Lösung dafür gibt es std::bit_cast (ist auch constexpr).

Sieht so aus, als würde das aber immer eine Kopie zurück geben. Käme 
also nicht ganz an unions oder reinterpret_casts dran.

von Wilhelm M. (wimalopaan)


Lesenswert?

NaNi schrieb:
> Rolf M. schrieb:
>> Im Grunde ja, aber es gibt die von mir beschriebene Ausnahme, die von C
>> übernommen wurde. Hauptsächlich wohl, um in etwa so was zu ermöglichen:
>
> Ah, ja. Jetzt verstehe ich was du meinst. Anscheinend gab es vor c11
> keine anonymen unions. Was diese Ausnahme vieleicht nötig gemacht hat.

Ich kenne zwar auch die o.g. Ausnahme mit der common-initial-sequence 
bei unions, aber trotzdem bleibt der Zugriff über das non-aktiv member, 
was UB ist. Zumindest scheint der g++ das so zu interpretieren.

Eine der besten Möglichkeiten herauszufinden, ob ein Konstrukt UB ist, 
es in einen constexpr-Kontext zu verfrachten (dazu hatte ich vor einiger 
Zeit (Jahren) mal Beispiele gemacht, die ich hier zwar auch gepostet 
hatte, nun aber gerade auf die Schnelle nicht wiederfinde). Denn in 
einem constexpr-Kontext ist kein UB erlaubt -> diagnostic required!

Daher gibt leider doch das obige Konstrukt (im g++) UB:
1
enum event_type {
2
    mouse_move, mouse_click
3
};
4
struct event {
5
    event_type type;
6
};
7
struct move_event {
8
    event_type type;
9
    int x, y;
10
};
11
struct click_event {
12
    event_type type;
13
    int button;
14
};
15
union event_union {
16
    event e;
17
    move_event move;
18
    click_event click;
19
};
20
21
constexpr int handle_event(const event_union* u) {
22
    switch (u->e.type) // Zugriff über anderes Element
23
    {
24
        case mouse_move:
25
            // handle u.move
26
            break;
27
        case mouse_click:
28
            // handle u.click
29
            break;
30
    }
31
    return 0;
32
}
33
int main() {
34
    constexpr event_union e {.click = {event_type::mouse_click, 1}};
35
    constexpr auto r = handle_event(&e); // <---- Fehler

Angeqendet auf das Beispiel des TO:
1
template<typename T>
2
union AB {
3
    A<T> a;
4
    B<T> b;
5
};
6
7
template<typename T>
8
constexpr char test() {
9
    AB<T> ab;
10
    ab.a.e0 = 1; 
11
    return ab.b.e0; // <--- UB wegen non-active member
12
}

> Wilhelm M. schrieb:
>> Als Lösung dafür gibt es std::bit_cast (ist auch constexpr).
>
> Sieht so aus, als würde das aber immer eine Kopie zurück geben. Käme
> also nicht ganz an unions oder reinterpret_casts dran.

Ich denke, das std::bit_cast hier (meistens) auf die Kopie verzichten 
wird, weil es eben genau für diesen Anwendungsfall (als built-in) 
gemacht worden ist. Das erkennen des punning-patterns ist genau seine 
Aufgabe. Im Gegensatz zu memcpy(), was das zwar auch (oft) erkennt, aber 
dann eben auch nicht constexpr ist.

Wie man oben sieht, betrachtet der g++ den Weg über unions (trotz der 
Ausnahme im C++-Standard) als UB, und das reinterpret_cast sowieso (was 
zudem auch nicht in einem constexpr-Kontext angewendet werden kann).

von Wilhelm M. (wimalopaan)


Lesenswert?

Leider kann ich den obigen Beitrag nicht löschen ...
Edieren kann ich ihn auch nicht mehr.

Er ist falsch, bitte nicht beachten.

von Wilhelm M. (wimalopaan)


Lesenswert?

Wenn Du homogene Datenelemente in den Klassen hast, kannst ggf. ein 
Array nehmen. Wenn Du weiterhin eine Basistemplate einführst, dann 
erhälst Du folgendes:
1
#include <cstdint>
2
#include <array>
3
4
template<typename T, size_t N>
5
struct Base {
6
    std::array<T, N> e;
7
    const T& operator[](size_t i) const {
8
        return e[i];
9
    }
10
};
11
template<typename T>
12
struct A : Base<T, 3> {    
13
};
14
15
template<typename T>
16
struct B : Base<T, 3> {   
17
};
18
int main() {
19
    const A<char> a{1, 2, 3};
20
    const B<char> b{4, 5, 6};
21
    
22
    return a[0] + b[0];
23
}

Vielleicht das Deine Anforderungen?

: Bearbeitet durch User
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.