Sicherer Vergleich von Ganzzahlen in C++20
Wenn vorzeichenbehaftete und vorzeichenlose Ganzzahlen verglichen werden, führt das häufig nicht zum gewünschten Ergebnis. Dank der sechs std::cmp_ *-Funktionen gibt es eine Heilung mit C++20.
Wenn vorzeichenbehaftete und vorzeichenlose Ganzzahlen verglichen werden, führt das häufig nicht zum gewünschten Ergebnis. Dank der sechs std::cmp_
*-Funktionen gibt es eine Heilung mit C++20.

Eventuell erinnerst du dich an die Regel "ES.100 Don't mix signed and unsigned arithmetic [1]" der C++ Core Guidelines. Ich habe in dem früheren Artikel "C++ Core Guidelines: Regeln zu Anweisungen und Arithmetik [2]" ein wenig darüber geschrieben. Heute möchte ich mich deutlich tiefer mit dem Problem beschäftigen, wenn vorzeichenbehaftete und vorzeichenlose Ganzzahlen verglichen werden.
Los geht es mit einem unsicheren Vergleich.
Unsicherer Vergleich von Ganzzahlen
Natürlich hat es einen Grund, dass das folgende Programm unsafeComparison.cpp
heißt:
// unsafeComparison.cpp
#include <iostream>
int main() {
std::cout << std::endl;
std::cout << std::boolalpha;
int x = -3; // (1)
unsigned int y = 7; // (2)
std::cout << "-3 < 7: " << (x < y) << std::endl;
std::cout << "-3 <= 7: " << (x <= y) << std::endl;
std::cout << "-3 > 7: " << (x > y) << std::endl;
std::cout << "-3 => 7: " << (x >= y) << std::endl;
std::cout << std::endl;
}
Das Ausführen des Programms birgt ein großes Überraschungspotenzial.

-3 soll größer als 7 sein! Eventuell kennst du bereits das Problem. Ich vergleiche signed int x
(Zeile (1)) mit unsingned int y
(Zeile (2)). Was passiert unter der Decke? Das folgende Programm gibt die Antwort:
// unsafeComparison2.cpp
int main() {
int x = -3;
unsigned int y = 7;
bool val = x < y; // (1)
static_assert(static_cast<unsigned int>(-3) == 4'294'967'293);
}
Im Beispiel fokussiere ich mich auf den Kleiner-als-Operator. C++ Insights gibt mir die folgende Ausgabe:

Die folgenden Schritte zeigen das Unheil:
- Der Compiler transformiert den Ausdruck
x <
y (Zeile (1)) instatic_cast<unsigned int>(x) < y
. Insbesondere wirdsigned int
in einunsigned int
konvertiert. - Dank der Konvertierung wird -3 zu 4'294'967'293.
- 4'294'967'293 entspricht (-3) modulo (2 hoch32).
- 32 ist die Anzahl der Bits eines
unsigned int
auf C++ Insights.
Dank C++20 können wir jetzt sichere Vergleiche einsetzen.
Sichere Vergleiche für Ganzzahlen
C++20 bietet sechs Vergleichsfunktionen für Ganzzahlen an.

Sie lassen es zu, das vorherige Programm unsafeComparison.cpp
in das Programm safeComparison.cpp
zu refaktorieren. Die neuen Vergleichsfunktionen benötigen die Header-Datei <utility
>
.
// safeComparison.cpp
#include <iostream>
#include <utility>
int main() {
std::cout << std::endl;
std::cout << std::boolalpha;
int x = -3;
unsigned int y = 7;
std::cout << "3 == 7: " << std::cmp_equal(x, y) << std::endl;
std::cout << "3 != 7: " << std::cmp_not_equal(x, y) << std::endl;
std::cout << "-3 < 7: " << std::cmp_less(x, y) << std::endl;
std::cout << "-3 <= 7: " << std::cmp_less_equal(x, y) << std::endl;
std::cout << "-3 > 7: " << std::cmp_greater(x, y) << std::endl;
std::cout << "-3 => 7: " << std::cmp_greater_equal(x, y) << std::endl;
std::cout << std::endl;
}
Ich habe der Vollständigkeit halber in dem Programm auch den Gleichheits- und Ungleichsoperator verwendet. Nun erhalte ich die erwartete Ausgabe:

Werden die Vergleichsfunktionen mit einem Nicht-Integer aufgerufen, moniert das der Compiler.
// safeComparison2.cpp
#include <iostream>
#include <utility>
int main() {
double x = -3.5; // (1)
unsigned int y = 7; // (2)
std::cout << "-3.5 < 7: " << std::cmp_less(x, y) << std::endl;
}
Der Versuch einen double
-Wert (Zeile (1)) mit einen unsigned in
tWert (Zeile (2)) zu vergleichen, gibt mit dem GCC-10-Compiler eine längliche Fehlermeldung. Hier sind die entscheidenden Zeilen der Fehlermeldung:

Das interne Type-Trait __is_standard_integer
führt zur Fehlermeldung. Nun bin ich neugierig: Wie ist das Type-Trait definiert? Ich habe es in der GCC-Type-TraitsImplementierung auf GitHub [3] gefunden. Dies sind die wichtigen Zeilen aus der Header-Datei <type_traits>
:
// Check if a type is one of the signed or unsigned integer types.
template<typename _Tp>
using __is_standard_integer
= __or_<__is_signed_integer<_Tp>, __is_unsigned_integer<_Tp>>;
// Check if a type is one of the signed integer types.
template<typename _Tp>
using __is_signed_integer = __is_one_of<__remove_cv_t<_Tp>,
signed char, signed short, signed int, signed long,
signed long long
// Check if a type is one of the unsigned integer types.
template<typename _Tp>
using __is_unsigned_integer = __is_one_of<__remove_cv_t<_Tp>,
unsigned char, unsigned short, unsigned int, unsigned long,
unsigned long long
__remove_cv_
t ist eine weitere interne Funktion des GCC, um const
oder volatile
von einem Datentyp zu entfernen. Ich denke, du bist auch neugierig und willst wissen, was passiert, wenn ein double
-Wert mit einem unsigned int
-Wert auf klassische Art verglichen wird.
Das Programm classicalComparison.cpp
vergleicht Datentypen, die nicht zusammenpassen:
// classicalComparison.cpp
int main() {
double x = -3.5;
unsigned int y = 7;
auto res = x < y; // true
}
Der Vergleich funktioniert. Der entscheidende Wert vom Typ unsigned in
t ist dank floating-point-
Promotion zu einem double-
Wert geworden. C++ Insights bringt die Wahrheit ans Licht:

Nach so vielen Vergleichen möchte ich diesen Artikel mit neuen mathematischen Konstanten in C++20 beenden.
Mathematische Konstanten
Zuerst einmal verlangen die mathematischen Konstanten die Header-Datei <numbers>
und den Namensraum std::numbers
. Die zwei Tabellen stellen alle mathematischen Konstanten vor:


Das Programm mathematicConstants.cpp
wendet die Konstanten an:
// mathematicConstants.cpp
#include <iomanip>
#include <iostream>
#include <numbers>
int main() {
std::cout << std::endl;
std::cout<< std::setprecision(10);
std::cout << "std::numbers::e: " << std::numbers::e << std::endl;
std::cout << "std::numbers::log2e: " << std::numbers::log2e << std::endl;
std::cout << "std::numbers::log10e: " << std::numbers::log10e << std::endl;
std::cout << "std::numbers::pi: " << std::numbers::pi << std::endl;
std::cout << "std::numbers::inv_pi: " << std::numbers::inv_pi << std::endl;
std::cout << "std::numbers::inv_sqrtpi: " << std::numbers::inv_sqrtpi << std::endl;
std::cout << "std::numbers::ln2: " << std::numbers::ln2 << std::endl;
std::cout << "std::numbers::sqrt2: " << std::numbers::sqrt2 << std::endl;
std::cout << "std::numbers::sqrt3: " << std::numbers::sqrt3 << std::endl;
std::cout << "std::numbers::inv_sqrt3: " << std::numbers::inv_sqrt3 << std::endl;
std::cout << "std::numbers::egamma: " << std::numbers::egamma << std::endl;
std::cout << "std::numbers::phi: " << std::numbers::phi << std::endl;
std::cout << std::endl;
}
Dies ist die Ausgabe mit dem MSVC Compiler 19.27:

Die mathematischen Konstanten gibt es für die Datentypen float
, double
und long
double
. Per default wird double
als Datentyp verwendet. Der Datentyp float
oder long
double
muss explizit gesetzt werden: std::numbers::pi_v<float>
oder std::numbers::pi_v<long double>
.
Wie geht's weiter?
C++20 bietet noch mehr praktische Funktionen an. Zum Beispiel lässt sich der Compiler anfragen, welche C++20-Features er bereits anbietet. Darüber hinaus erlaubt std::bind_front
es, auf einfache Art Funktionsobjekte zu erzeugen. Dank std::is_constant_evaluated
ist es möglich unterschiedliche Zweige des Codes zu prozessieren. Die Entscheidung hängt davon ab, ob die Funktion zur Compilezeit oder Laufzeit ausgeführt wird.
( [4])
URL dieses Artikels:
https://www.heise.de/-4967401
Links in diesem Artikel:
[1] https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-mix
[2] https://heise.de/-3997787
[3] https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/include/std/type_traits
[4] mailto:rainer@grimm-jaud.de
Copyright © 2020 Heise Medien