Sicherer Vergleich von Ganzzahlen in C++20

Modernes C++ Rainer Grimm  –  185 Kommentare

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" der C++ Core Guidelines. Ich habe in dem früheren Artikel "C++ Core Guidelines: Regeln zu Anweisungen und Arithmetik" 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:

  1. Der Compiler transformiert den Ausdruck x < y (Zeile (1)) in static_cast<unsigned int>(x) < y. Insbesondere wird signed int in ein unsigned int konvertiert.
  2. Dank der Konvertierung wird -3 zu 4'294'967'293.
  3. 4'294'967'293 entspricht (-3) modulo (2 hoch32).
  4. 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 intWert (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 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 int 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.