Es scheint, als gäbe es zwei Arten von C++. Das praktische C++ und der Sprachjurist C++. In bestimmten Situationen kann es hilfreich sein, ein Bitmuster eines Typs so interpretieren zu können, als wäre es ein anderer Typ. Fließkommatricks sind ein bemerkenswertes Beispiel. Nehmen wir die berühmte schnelle inverse Quadratwurzel (entnommen aus Wikipediadie wiederum entnommen wurde hier):
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
return y;
}
Abgesehen von Details verwendet es bestimmte Eigenschaften der IEEE-754-Fließkommabitdarstellung. Der interessante Teil hier ist die *(long*)
gegossen aus float*
zu long*
. Es gibt Unterschiede zwischen C und C++ darüber, welche Typen solcher Umwandlungen zur Neuinterpretation definiertes Verhalten sind, aber in der Praxis werden solche Techniken oft in beiden Sprachen verwendet.
Die Sache ist die, dass es bei einem so einfachen Problem viele Fallstricke gibt, die mit dem oben vorgestellten Ansatz und verschiedenen anderen auftreten können. Um einige zu nennen:
- unspezifisches Verhalten
- striktes Aliasing
- lebenslange Probleme
- Endianität
- Ausrichtung
Gleichzeitig gibt es viele Möglichkeiten, Typ-Wortspiele durchzuführen, und viele Mechanismen, die damit zusammenhängen. Das sind alles, was ich finden konnte:
-
reinterpret_cast
und Besetzung im C-Stil[[nodiscard]] float int_to_float1(int x) noexcept { return *reinterpret_cast<float*>(&x); } [[nodiscard]] float int_to_float2(int x) noexcept { return *(float*)(&x); }
-
static_cast
undvoid*
[[nodiscard]] float int_to_float3(int x) noexcept { return *static_cast<float*>(static_cast<void*>(&x)); }
-
std::bit_cast
[[nodiscard]] constexpr float int_to_float4(int x) noexcept { return std::bit_cast<float>(x); }
-
memcpy
[[nodiscard]] float int_to_float5(int x) noexcept { float destination; memcpy(&destination, &x, sizeof(x)); return destination; }
-
union
[[nodiscard]] float int_to_float6(int x) noexcept { union { int as_int; float as_float; } destination{x}; return destination.as_float; }
-
Platzierung
new
undstd::launder
[[nodiscard]] float int_to_float7(int x) noexcept { new(&x) float; return *std::launder(reinterpret_cast<float*>(&x)); }
-
std::byte
[[nodiscard]] float int_to_float8(int x) noexcept { return *reinterpret_cast<float*>(reinterpret_cast<std::byte*>(&x)); }
Die Frage ist, welche dieser Wege sicher, welche unsicher und welche für immer verdammt sind. Welche sollte verwendet werden und warum? Gibt es eine kanonische, die von der C++-Community akzeptiert wird? Warum führen neue Versionen von C++ noch mehr Mechanismen ein? std::launder
in C++17 bzw std::byte
, std::bit_cast
in C++20?
Um ein konkretes Problem zu nennen: Was wäre der sicherste, leistungsfähigste und beste Weg, um die schnelle inverse Quadratwurzelfunktion neu zu schreiben? (Ja, ich weiß, dass es auf Wikipedia einen Vorschlag für einen Weg gibt).
Bearbeiten: Um zur Verwirrung beizutragen, scheint es so zu sein ein Vorschlag das schlägt vor, einen weiteren Wortspielmechanismus hinzuzufügen: std::start_lifetime_as
was auch in einer anderen Frage diskutiert wird.
Was Sie als praktischen vs. Sprachanwalt bezeichnen, kümmert sich tatsächlich um die Portabilität oder nicht. Sie können untersuchen, was der Compiler tut, wenn der Standard behauptet, es sei UB, aber dann sind Sie an diesen Compiler, einen bestimmten Satz von Compileroptionen und eine Zielplattform gebunden. Ich bin kein Anwalt für Sprachen, aber ich muss Code schreiben, der, wenn er hier kompiliert wird, auch dort kompiliert wird. Und das ist eine sehr praktische Ansicht
– 463035818_ist_keine_Nummer
21. Mai 2021 um 11:53 Uhr
Ich denke nur
std::bit_cast
undmemcpy
sind nicht UB.– Jarod42
21. Mai 2021 um 11:54 Uhr
std::bit_cast
ist nur C++20 und höher… Ist aber sicher das modern Weg.– Serge Ballesta
21. Mai 2021 um 12:01 Uhr
btw “Allerdings gilt Typ-Wortspiel durch eine Union als schlechte Praxis in C++.” aus Wikipedia ist nicht ganz richtig. Type punning through a union ist in C++ undefiniert, obwohl viele Compiler es als Erweiterung anbieten
– 463035818_ist_keine_Nummer
21. Mai 2021 um 12:30 Uhr
Auch wenn die Motivation, dies zu Optimierungszwecken (z. B. schnelle inverse Quadratwurzeln) zu tun, im Laufe der Zeit verflogen ist, heißt das nicht, dass ein moderner C++-Programmierer niemals Typ-Wortspiele machen muss. Es ist in der eingebetteten Entwicklung ziemlich üblich.
– Cody Grey
♦
21. Mai 2021 um 20:41 Uhr