C++ enum VS #define

firespot schrieb:
Somit ist nicht garantiert dass in write der ursprüngliche integer-Wert wiederhergestellt werden kann.
Nein, das ist nicht was das bedeuetet. Punkt 3 bedeutet, dass nicht garantiert wird, dass man einen Integer in einen Pointer casten kann, und dann wieder zurück in einen Integer, der den selben Wert hat. Als Code:
C++:
const auto val = intptr_t{1234};
const auto* val_as_ptr = reinterpret_cast<void*>(val);
const auto val_from_ptr = reinterpret_cast<intptr_t>(val_as_ptr);
assert(val == val_from_ptr); // Not guaranteed

Es geht in dem Code von mir ja nicht darum einen Integer in einem Pointer-Type zu speichern, sondern nur darum einen const uint8_t* in einen const char* umzuwandeln, und das ist auf jeden Fall standardkonform. Der Standard garantiert, dass man char*, und unsigned char*, und deshalb auch uint8_t* (typedef für unsigned char), und std::byte* (seit C++17) verwenden darf um raw data anzusehen.

https://en.cppreference.com/w/cpp/language/types
unsigned char - type for unsigned character representation. Also used to inspect object representations (raw memory).
https://en.cppreference.com/w/cpp/types/byte
Like char and unsigned char, it can be used to access raw memory occupied by other objects (object representation)

Das darf man aber nur mit diesen Typen machen. Für alles andere gelten die strict aliasing Regeln von C++.

firespot schrieb:
Und wenn write nur die ersten length-bytes vom übergebenen pointer-Wert nutzt, frage ich mich ob je nach little oder big endian es dann auch garantiert die "richtigen" bytes nutzt.
Endianness spielt keine Rolle im Kontext von einem einzigen Byte. Und sizeof(uint8_t) == sizeof(char) == 1, garantiert der Standard. Es wird also immer nur 1 Byte von write angesehen. Nämlich wird das uint8_t byte, aliased als char von write angesehen, und das ist okay, weil der Standard sagt:

https://en.cppreference.com/w/cpp/language/types
For every value of type unsigned char in range [0, 255], converting the value to char and then back to unsigned char produces the original value.

firespot schrieb:
Solange wir aber nicht genau wissen, was write intern überhaupt mit den Parametern macht und in was es data zurückcastet, ist es generell schwierig hier komformen Code aufzusetzen.
Wir müssen natürlich voraussetzen, dass die write Funktion selbst standardkonform ist, das sollte soweit klar sein. Aber sofern das gegeben ist, ist es kein Problem damit standardkonformen Code zu schreiben. Zum Beispiel könnte man:

C++:
const auto val = 1234;
i2cInterface->write(i2cAddress, reinterpret_cast<const char*>(&val), sizeof(val), true);
machen, und das ist standardkonform. Ja, jetzt spielt die Endianness eine Rolle, zumindest für den Empfänger dieser I2C Message. Das ist erlaubt, weil man eben mit char raw data ansehen darf, also kann man auch die einzelnen Bytes eines Integer damit ansehen. Das gilt auch für beliebige C++ Objekte, könnte also auch eine Klasse sein, statt einem Integer.

Gruß
BlackMark
 
  • Gefällt mir
Reaktionen: Vulpecula, firespot und nullPtr
firespot schrieb:
[...] bringt dir in dem Fall die Deklaration als
C++:
enum class
aber eigentlich nix, da du keinen neuen Typ fürs Typsystem kreieren willst, [...]
Ich nehme stark an, dass das sensor sampling (das Beispiel aus dem initialen Post) in die API wandern wird.
 
  • Gefällt mir
Reaktionen: Vulpecula
In C (nicht C++) gibt es keinen portablen Weg, den "underlying type" eines enums zu spezifizieren - er wird im Standard schlichtweg als "int" definiert (§ 6.7.2.2 Enumeration specifiers). Die #define-Variante bietet hier die Möglichkeit, das ganze als Integer Constant mit Typ (§ 6.4.4.1 Integer constants) zu definieren.

Für C++ geht das eleganter, das wurde hier ja bereits mehrfach beschrieben.
 
  • Gefällt mir
Reaktionen: Vulpecula
BlackMark schrieb:
Nein, das ist nicht was das bedeuetet. Punkt 3 bedeutet, dass nicht garantiert wird, dass man einen Integer in einen Pointer casten kann, und dann wieder zurück in einen Integer, der den selben Wert hat. Als Code:
C++:
const auto val = intptr_t{1234};
const auto* val_as_ptr = reinterpret_cast<void*>(val);
const auto val_from_ptr = reinterpret_cast<intptr_t>(val_as_ptr);
assert(val == val_from_ptr); // Not guaranteed

Es geht in dem Code von mir ja nicht darum einen Integer in einem Pointer-Type zu speichern, sondern nur darum einen const uint8_t* in einen const char* umzuwandeln, und das ist auf jeden Fall standardkonform.

Dein Beispiel war zwar genau die Illegalität auf die hinweisen wollte, aber ich hab in meiner response die C++-Code-Beispiele hier im Faden verwechselt - mea culpa.

Ich wollte mich eigentlich auf das Bsp. in #12 beziehen, wo direkt von integer auf pointer gecastet wird, was nicht geht:
KitKat::new() schrieb:
Code:
namespace Registers {
    static const char * CHIP_ID_REGISTER_ADDRESS = reinterpret_cast<char*>(0x07);
}
int main(){
    write(23, Registers::CHIP_ID_REGISTER_ADDRESS, 1, true);
}
 
  • Gefällt mir
Reaktionen: Vulpecula und BlackMark
@firespot
firespot schrieb:
Der guten Praxis halber solltest du dir angewöhnen, die Länge der zu übermittelnden Daten immer über ein sizeof oder äquivalentes zu ermittlen und nicht als "magische Konstanten" angeben, welche zwar für die aktuelle Situation passen aber a) wenig ausdrucksstark sind für was sie eigentlich stehen, und b) Tür und Tor für bugs in späteren Modifikationen öffnen.

Ja, vielen Dank für den Hinweis. Mir ist durchaus bewusst, dass man "magic numbers" vermeiden sollte, habe es aber in diesem Zusammenhang erstmal ignoriert. Später, wenn nicht nur eines sondern mehrere Bytes herumgeschoben werden müssen (Temperatur und Luftdruck setzen sich aus jeweils drei Bytes zusammen), wäre es aber nötig geworden, mit sizeof() zu arbeiten.

Ich frage mich gerade, was sizeof() für structs ausgeben würde. Konkret: Ich muss die Kalibrierungs-Koeffizienten aus dem Speicher des Sensors auslesen und würde das ganze in einem struct speichern:

C++:
struct
{
    uint16_t dig_T1;
    int16_t  dig_T2;
    int16_t  dig_T3;
    uint16_t dig_P1;
    int16_t  dig_P2;
    int16_t  dig_P3;
    int16_t  dig_P4;
    int16_t  dig_P5;
    int16_t  dig_P6;
    int16_t  dig_P7;
    int16_t  dig_P8;
    int16_t  dig_P9;
} calibrationCoefficients;

Gibt es dann sowas wie einen "Overhead", oder liefert sizeof() hier die tatsächliche Anzahl an Bytes zurück? Letzteres würde das auslesen deutlich vereinfachen, denn dann würde meine Funktion zum auslesen und wegspeichern dieser Koeffizienten deutlich eleganter ausfallen.
 
Vulpecula schrieb:
Ich frage mich gerade, was sizeof() für structs ausgeben würde. ...
Gibt es dann sowas wie einen "Overhead", oder liefert sizeof() hier die tatsächliche Anzahl an Bytes zurück?

sizeof() einer struct gibt dir die Größe des Objekts in bytes zurück, und zwar inklusive alignment für die data members und padding für ein array von Objekten dieser struct.

Salopp gesagt heißt dass, dass sich sizeof aus der Summe der sizeof der einzelnen data-members zusammensetzt plus einzelnen bytes, die dafür sorgen dass jedes data member ein "gutes/natürliches" alignment hat (inkl. wenn in einem array).

Ein Bsp. sagt vielleicht mehr als tausend Worte:

C++:
struxt x1 {   
    uint16_t a;
    uint16_t b;   
};

sizeof(uint16_t) ist 2, das ganze 2x macht 4. Da ein uint16_t ein "natürliches" alignment von 2 hat braucht man keine extra padding-bytes mehr, und das sizeof der struct ist 4.

C++:
struxt x2 {   
    uint16_t a;
    char c;
    uint16_t b;   
};

Hier haben wir für die beiden uint16_t 2x ein sizeof von 2, und für char ein sizeof von 1. Da aber b ein "natürliches" alignment von 2 hat, wird der Compiler also ein padding-byte zwischen c und b einpacken, und damit wird die struct ein sizeof von 6 haben.

C++:
struxt x3 {   
    uint16_t * a;
    char c;
};

Auf einer 32-bit Plattform wird sizeof(a) wahrscheinlich 4 sein, und auf einer 64-bit Plattform wahrscheinlich 8. Dazu kommt noch ein byte für c. Da die struct aber auch innerhalb eines arrays ein korrektes alignment braucht, werden nach c soviele padding-bytes eingefügt dass a auch dann ein natürliches alignment hat wenn es eben das x-te Element eines arrays ist. Effektiv wird also diese struct also 2x sizeof(uint16_t *) haben, also (je nach Plattform) 8 oder 16.


C++:
struxt x4 {   
    uint16_t * a;
    char b;
    uint16_t * c;
    char d;
    uint16_t * e;
    char f;
};

Ähnliches Spiel wie oben; wegen den padding-bytes wird die struct 6*sizeof(uint16_t *) haben.


C++:
struxt x5 {   
    uint16_t * a;
    uint16_t * c;
    uint16_t * e;
    char b;
    char d;
    char f;
};

Jetzt sind dieselben data-members wie oben im Spiel, aber in anderer Deklarationsreihenfolge. c und e haben ihr natürliches alignment sowieso, und b, d und f auch. Daher wird der compiler nur am Ende padding-bytes einfügen damit auch im array ein alignment garantiert ist. Somit wird sizeof dieser struct (wahrscheinlich) 4*sizeof(uint16_t *) sein.

Der Compiler muss die data members in der Reihenfolge der Deklaration anlegen. Er kann also nicht x4 so umstrukturieren, dass die data members platzsparender (wie in x5) angeordnet wären.
Falls es dir auf speicherknappen Systemen um eine möglichst platzsparende Anordnung der data members geht, lautet die Faustregel folgendermaßen: Sortiere sie entsprechend ihrer individuellen sizeof-s (also wie in x5).
 
  • Gefällt mir
Reaktionen: BlackMark
Es gibt (noch?) keinen Standardkonformen Weg ein arbiträres struct Layout zu erzwingen, wie @firespot schon erklärt hat, darf/muss der Compiler Padding einfügen um die Alignment Requirements der verwendeten Typen zu garantieren. Weil es aber ein häufiger Use-case ist, dass ein struct ein gewisses Layout hat, gibt es dafür non-standard Compiler Extensions. Das nennt sich dann Struct-packing und führt dazu, dass kein Padding eingefügt wird.

Als Beispiel:
https://godbolt.org/z/zf5Pa1GTW
C++:
#include <stdint.h>

struct [[gnu::packed]] A {
    uint8_t a;
    uint16_t b;
    uint8_t c;
};

static_assert(sizeof(A) == 4);

Es gibt auch Compiler Flags die erzwingen, dass jedes struct immer gepacked wird. Für gcc wäre das zB -fpack-struct.

Man muss sich aber dann natürlich bewusst sein, dass dieses Verhalten vom jeweiligen Compiler abhängt und im Allgemeinen nicht portabel ist. Wenn man Code für einen spezifischen Microcontroller schreibt, und mit immer dem gleichen Compiler kompiliert, dann ist das aber auch kein Problem. Dann kann man auch so ein struct verwenden um das Register Layout von Hardware mit der man kommuniziert darzustellen.

Die andere Richtung, wenn man also mehr Padding haben will als der Compiler einfügt, ist seit C++11 aber Standardkonform. Man kann mit alignas ein größeres Alignment erzwingen.

Gruß
BlackMark
 
  • Gefällt mir
Reaktionen: firespot
Zurück
Oben