Forum: PC-Programmierung C++, Unions und UB


von Raoul D. (raoul_d219)


Lesenswert?

Hallo zusammen,

folgendes kleines MVCE sei gegeben:
1
#include <stdint.h>
2
3
template<typename T>
4
struct C {
5
    union {
6
        volatile T a;
7
        volatile T b;
8
    };
9
    static constexpr uint8_t address = 0x28;
10
};
11
12
template<typename C>
13
constexpr inline C* getBaseAddr() {
14
    return reinterpret_cast<C*>(C::address);
15
}
16
17
volatile uint8_t g = 0;
18
19
int main() {
20
    constexpr auto c1 = getBaseAddr<C<uint8_t>>;
21
    
22
    c1()->a = 1;
23
    g = c1()->b;    
24
    
25
    while(true) {}
26
}

Mit z.B. avr-g++ (7.0.0) funktioniert es wie es soll.

Allerdings ist Datenelement b nicht das aktive Datenelement der union im 
Moment des Auswertens. Nach (oberflächlichem) Lesen der betreffenden 
Passagen des C++-Standards könnte das ein Grund für UB sein.
Bin mir aber unsicher ...

von tictactoe (Gast)


Lesenswert?

Funktioniert das wirklich? Was ist c1()?

Sagen wir mal, da wäre nur c1 statt c1(). Dann handelt es sich 
tatsächlich um Undefined Behavior bei g = c1->b.

von Raoul D. (raoul_d219)


Lesenswert?

tictactoe schrieb:
> Funktioniert das wirklich?

Ja.

> Was ist c1()?

Steht doch da ...

> Sagen wir mal, da wäre nur c1 statt c1(). Dann handelt es sich
> tatsächlich um Undefined Behavior bei g = c1->b.

Nein, dann wäre es syntaktisch falsch.

von Mikro 7. (mikro77)


Lesenswert?

Ist das absichtlich so konfus geschrieben: "C" als Bezeichner einer 
struct, sowie als Template Argument. "c1" als Bezeichner für eine 
Funktion die immer die Adresse 0x28 zurückliefert.

Was steht denn an Adresse 0x28 wo du deine struct hincastest? Bei meinem 
"normalen" System läuft es auf eine Seg fault.

von Raoul D. (raoul_d219)


Lesenswert?

Mikro 7. schrieb:

> Was steht denn an Adresse 0x28 wo du deine struct hincastest? Bei meinem
> "normalen" System läuft es auf eine Seg fault.

Ja, das ist klar. Aber darum geht es ja auch nicht.

Habe das Beispiel etwas "entschärft":
1
#include <stdint.h>
2
3
struct Component {
4
    typedef uint8_t value_type;
5
    union {
6
        volatile value_type a;
7
        volatile value_type b;
8
    };
9
};
10
11
volatile Component c1;
12
volatile uint8_t g = 0;
13
14
int main() {
15
    c1.a = 1;
16
    g = c1.b;    
17
    
18
    while(true) {}
19
}

Das erste Beispiel hätte ich vllt im µC / Elektronik Forum posten 
sollen, weil die Adresse natürlich etwas mit einer Resgisteradresse zu 
tun hat.

Bei der obigen Version ist es aber egal. Die Frage ist dieselbe.

von Mikro 7. (mikro77)


Lesenswert?

Ich nehme an, du erwartest g==1? Mit gcc scheint das auch zu klappen. 
Bin mir aber nicht sicher ob das gegen Aliasing verstößt. Wird bspw. 
hier diskutiert: 
http://stackoverflow.com/questions/2906365/gcc-strict-aliasing-and-casting-through-a-union#2959055

von Raoul D. (raoul_d219)


Lesenswert?

Mikro 7. schrieb:
> Ich nehme an, du erwartest g==1? Mit gcc scheint das auch zu klappen.

In C11 ist ein sog. "typ-punning" explizit erlaubt.

In C++(98/11/14/17) explizit nicht. Allerdings gibt es eben Ausnahmen.

Eine Ausnahme besteht für Standard-Layout-Type mit einer gleichen 
Abfolge der Datenelement. In diesem Fall soll das Lesen über das 
nicht-aktive Datenelement (hier b) ok sein.

Der nächste Schritt wäre dann auch die Modifikation über das 
nicht-aktive Datenelement b (was damit zum aktiven wird, weil zulesetzt 
geschrieben) und dann Lesen über a.

Auch dies scheint in allen Fällen beim g++ zu funktionieren, stellt aber 
i.A. UB dar, jedenfalls so, wie ich das im Standard lese.

von tictactoe (Gast)


Lesenswert?

Raoul D. schrieb:
>> Was ist c1()?
>
> Steht doch da ...

Soweit ich sehe, ist es ein Pointer auf struct C. Seit wann kann man 
einen Pointer wie eine Funktion aufrufen?

Aber egal...

Raoul D. schrieb:
> Eine Ausnahme besteht für Standard-Layout-Type mit einer gleichen
> Abfolge der Datenelement. In diesem Fall soll das Lesen über das
> nicht-aktive Datenelement (hier b) ok sein.

Zumindest in C++ gibt es keine Ausnahme: Man darf nur den Union-Member 
auslesen, der zuletzt beschrieben worden ist. Alles andere ist UB. Es 
ist egal, ob die Members Standard-Layout-Typen sind oder nicht.

von guest (Gast)


Lesenswert?

tictactoe schrieb:
> Soweit ich sehe, ist es ein Pointer auf struct C. Seit wann kann man
> einen Pointer wie eine Funktion aufrufen?

Nö, das ist ein Pointer auf eine parameterlose Funktion die einen 
Pointer auf struct C zurückliefert.

von Daniel A. (daniel-a)


Lesenswert?

tictactoe schrieb:
> Raoul D. schrieb:
>>> Was ist c1()?
>>
>> Steht doch da ...
>
> Soweit ich sehe, ist es ein Pointer auf struct C. Seit wann kann man
> einen Pointer wie eine Funktion aufrufen?

Nein, Betrachte den code nochmal:
1
constexpr auto c1 = getBaseAddr<C<uint8_t>>;
Die Funktion wird nicht aufgerufen, also ist c1 ein Funktionspointer auf 
die Funktion.

von Mikro 7. (mikro77)


Lesenswert?

Raoul D. schrieb:
> In C11 ist ein sog. "typ-punning" explizit erlaubt.
>
> In C++(98/11/14/17) explizit nicht. Allerdings gibt es eben Ausnahmen.

Ich würde die Frage auf Stackoverflow einstellen. Am besten mit 'nem 
(bereinigten) Pointer Cast Beispiel (aus dem ersten Post -- das ist wohl 
etwas komplexer), falls sich Experten wie KHB oder Yalu etc. hier nicht 
noch melden. -- Wenn du's dort einstellst wäre ein Link nett :-).

von Raoul D. (raoul_d219)


Lesenswert?

tictactoe schrieb:
> Raoul D. schrieb:
>>> Was ist c1()?
>>
>> Steht doch da ...
>
> Soweit ich sehe, ist es ein Pointer auf struct C. Seit wann kann man
> einen Pointer wie eine Funktion aufrufen?
>
> Aber egal...
>
> Raoul D. schrieb:
>> Eine Ausnahme besteht für Standard-Layout-Type mit einer gleichen
>> Abfolge der Datenelement. In diesem Fall soll das Lesen über das
>> nicht-aktive Datenelement (hier b) ok sein.
>
> Zumindest in C++ gibt es keine Ausnahme: Man darf nur den Union-Member
> auslesen, der zuletzt beschrieben worden ist. Alles andere ist UB. Es
> ist egal, ob die Members Standard-Layout-Typen sind oder nicht.

Das lese ich etwas anders (C++, N4297, Kapitel 9.5 Unions):
1
In a union, at most one of the non-static data members can be active at any time, that is, the value of at
2
most one of the non-static data members can be stored in a union at any time. [ Note: One special guarantee
3
is made in order to simplify the use of unions: If a standard-layout union contains several standard-layout
4
structs that share a common initial sequence (9.2), and if an object of this standard-layout union type
5
contains one of the standard-layout structs, it is permitted to inspect the common initial sequence of any of
6
standard-layout struct members; see 9.2. — end note ]

von tictactoe (Gast)


Lesenswert?

Daniel A. schrieb:
> Nein, Betrachte den code nochmal:
> constexpr auto c1 = getBaseAddr<C<uint8_t>>;

Jetzt seh ich's. Keine Klammern nach dem Ausdruck auf der rechten Seite. 
Danke.

von tictactoe (Gast)


Lesenswert?

Raoul D. schrieb:
> Das lese ich etwas anders (C++, N4297, Kapitel 9.5 Unions):

Ja, stimmt. Das habe ich vergessen. Dein Use-case entspricht dem 
Szenario mit der "common initial sequence".

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Da die Komponenten volatile sind, hat der Compiler eh keine andere Wahl 
als Code zu generieren, die die Komponente zurückliest.

von Wilhelm M. (wimalopaan)


Lesenswert?

Johann L. schrieb:
> Da die Komponenten volatile sind, hat der Compiler eh keine andere Wahl
> als Code zu generieren, die die Komponente zurückliest.

Das "denke" ich auch, allerdings finde ich dafür auch keine Bestätigung 
im Standard oder sonstwo.

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.