Entwicklung & Code
C++26-Reflexion zur Kompilierungszeit | heise online
Im heutigen Beitrag meines C++-Blogs möchte ich auf die statische Reflexion in C++26 eingehen. Einer der großen Vorteile von Reflexion ist, dass wir die neue Funktion bereits ausprobieren können, da mit Clang ein Compiler verfügbar ist, der alle Facetten implementiert. Und auch der jüngst erschienene GCC 16 implementiert das Feature bereits.
Weiterlesen nach der Anzeige
Andreas Fertig ist erfahrener C++-Trainer und Berater, der weltweit Präsenz- sowie Remote-Kurse anbietet. Er engagiert sich im C++-Standardisierungskomitee und spricht regelmäßig auf internationalen Konferenzen. Mit C++ Insights ( hat er ein international anerkanntes Tool entwickelt, das C++-Programmierenden hilft, C++ noch besser zu verstehen.
Anhand meiner typischen Anwendungsfälle habe ich untersucht, welche Aufgaben sich mit Reflexion besser lösen lassen. Eine Aufgabe, die mich den größten Teil meiner Karriere beschäftigt hat, war das Lesen und Schreiben von Daten, die über eine Netzwerkverbindung kamen. Die Definition von „Netzwerk“ war zu verschiedenen Zeiten unterschiedlich. Gemeinsam war jedoch, dass alles, was gesendet wurde, in Netzwerk-Byte-Reihenfolge (Big-Endian) verschickt wurde, und alles, was empfangen wurde, ebenfalls in Netzwerk-Byte-Reihenfolge ankam. Für manche Systeme machte das keinen Unterschied, da sie bereits Big-Endian-Maschinen waren. Aber nicht immer. Vor allem ARM setzte auf Little-Endian. Bei einem Teil der Systeme mussten die Daten beim Empfangen oder Senden daher byteweise vertauscht werden.
Wie tauscht man Daten?
Das Problem hier (war), wie tauscht man die Daten? Jeder Datentyp, der größer als ein Byte ist, muss jedes Mal getauscht werden. C++23 hat uns std::byteswap gegeben, wodurch POSIX-Funktionen wie htons überflüssig geworden sind. Das bedeutet zumindest eine Verbesserung im Hinblick auf Sicherheit.
Fangen wir mit einem Beispiel an. Der folgende Code zeigt die Datenstrukturen, die ich heute verwende.
enum class Color16 : uint16_t
{
};
struct RGB48 {
Color16 red;
Color16 green;
Color16 blue;
auto operator<=>(const RGB48&) const = default;
};
struct LEDState {
bool state;
RGB48 rgbColor;
auto operator<=>(const LEDState&) const = default;
};
struct LEDStateMessage {
uint32_t messageId;
LEDState state;
auto operator<=>(const LEDStateMessage&) const = default;
};
Wie du sehen kannst, ist die letzte Struktur LEDStateMessage später die Wurzel. Diese Struktur enthält eine Struktur, die wiederum eine Struktur enthält. Wir gehen es heute also rekursiv an.
Weiterlesen nach der Anzeige
Alle structs verfügen über einen Dreiwege-Vergleichsoperator – das erleichtert die Überprüfung.
Was ich erreichen möchte, sieht in etwa so aus:
LEDStateMessage msg{.messageId = 3,
.state{.state = true,
.rgbColor{.red = Color16{0x5u},
.green = Color16{0x28u},
.blue = Color16{0x40u}}}};
Serialize(msg);
Die Daten sollten ausgetauscht oder besser noch an Ort und Stelle serialisiert werden. Ich gehe von einem System mit Speicherbeschränkungen aus, bei dem ich es mir nicht leisten kann, doppelt so viel Speicher zu verbrauchen.
Denke jetzt mal einen Moment darüber nach, wie viel Mühe du dir in der Vergangenheit gegeben hast, damit Serialize funktioniert. Ich für meinen Teil kann sagen, dass es mich viele Stunden gekostet hat.
Reflexion zur Rettung
Bist du bereit? Okay, hier kommt es:
template
requires(not std::is_pointer_v) and std::is_trivially_copyable_v
constexpr void Serialize(T& data)
{
if constexpr(std::is_array_v) { // #A
for(int i{}; i < std::size(data); ++i) { Serialize(data[i]); }
} else if constexpr(std::is_enum_v) { // #B
data = static_cast(std::byteswap(std::to_underlying(data)));
} else if constexpr(not std::is_class_v) { // #C
data = std::byteswap(data);
} else { // #D
static constexpr auto members =
std::define_static_array(std::meta::nonstatic_data_members_of(
^^T, std::meta::access_context::current()));
template for(constexpr auto& mem : members) { Serialize(data.[:mem:]); }
}
}
Die Implementierung von Serialize besteht aus vier Teilen. In Abschnitt #A prüft Serialize, ob es sich um ein Array handelt. Wenn der Datentyp passt, ruft die Implementierung Serialize für jedes Element des Arrays auf.
Als Nächstes prüft die Implementierung in #B, ob der Datentyp ein enum ist. In diesem Fall wird C++23s std::to_underlying zusammen mit std::byteswap verwendet, um die Bytes tatsächlich zu vertauschen.
Nun wird in #C, falls dieser Datentyp kein Klassentyp ist, std::byteswap auf die Daten angewendet.
So weit, so gut. Bisher ist das alles generischer Code, den du schon vor C++26 so schreiben konntest. Der knifflige Teil sind die Datenelemente von Klassentypen. Genau das wird in #D behandelt.
Dank der Reflexion-Fähigkeiten durchläuft die Implementierung alle Datenelemente der Klasse und ruft für jedes davon Serialize auf. Et voilà, ein hervorragender erster Entwurf einer generischen Serialisierungsfunktion.
(Matthias Parbel)