C++23: Die kleinen Perlen in der Kernsprache
Neben Deducing This gibt es in C++23 zahlreiche kleine, aber feine Neuerungen wie if consteval und literale Suffixe.
- Rainer Grimm
Die Kernsprache C++23 hat mehr zu bieten als Deducing This. Heute werde ich ĂĽber die kleinen Perlen schreiben.
Literale Suffixe
C++23 bietet neue integrale literale Suffixe fĂĽr (vorzeichenbehaftete) std::size_t
an.
std::size_t (C++17) ist ein vorzeichenloser Datentyp, der die maximale Größe eines beliebigen Typs enthalten kann. Er wird häufig zum Indexieren von Arrays und das Zählen in Schleifen verwendet.
Ein Beisspiel für den Einsatz ist das Iterieren durch einen Vektor. Aus Optimierungsgründen speichert man dessen Größe im Cache.
#include <vector>
int main() {
std::vector<int> v{0, 1, 2, 3};
for (auto i = 0, s = v.size(); i < s; ++i) {
/* use both i and v[i] */
}
}
Beim Kompilieren erscheint folgende Fehlermeldung im Compiler Explorer:
Der Grund dafĂĽr ist, dass auto i
in int
und s
in long unsigned int
abgeleitet hat. Folglich wird das Problem auch nicht behoben, wenn beide Variablen vorzeichenlos sind.
#include <vector>
int main() {
std::vector<int> v{0, 1, 2, 3};
for (auto i = 0u, s = v.size(); i < s; ++i) {
/* use both i and v[i] */
}
}
Jetzt leitet der Compiler i
zu unsigned int
, aber s
zu long unsigned int
ab. Der folgende Screenshot zeigt noch einmal die Fehlerausgabe des Compiler Explorers.
C++23 behebt dieses Problem durch das neue Literal Suffix z
.
#include <vector>
int main() {
std::vector<int> v{0, 1, 2, 3};
for (auto i = 0uz, s = v.size(); i < s; ++i) {
/* use both i and v[i] */
}
}
Dieses Beispiel basiert auf dem Proposal P0330R8. Er enthält weitere motivierende Beispiele für die neuen Literal-Suffixe.
if consteval
if consteval
verhält sich wie if (std::is_constant_evaluated()) { }
, besitzt aber ein paar Vorteile:
- Es wird kein Header
<type_traits>
benötigt. - Es besitzt eine einfachere Syntax als
std::is_constant_evaluated.
- Es kann verwendet werden, um immediate Funktionen (
consteval
Funktionen) aufzurufen.
std::is_constant_evaluated
ist eine C++20 Funktion, die erkennt, ob eine constexpr
Funktion während der Compilezeit ausgeführt wird.
Auf cppreference.com/is_constant_evaluated findet sich hierzu ein hervorragendes Beispiel:
#include <cmath>
#include <iostream>
#include <type_traits>
constexpr double power(double b, int x)
{
if (std::is_constant_evaluated() && !(b == 0.0 && x < 0))
{
// A constant-evaluation context:
// Use a constexpr-friendly algorithm.
if (x == 0)
return 1.0;
double r {1.0};
double p {x > 0 ? b : 1.0 / b};
for (auto u = unsigned(x > 0 ? x : -x); u != 0; u /= 2)
{
if (u & 1)
r *= p;
p *= p;
}
return r;
}
else
{
// Let the code generator figure it out.
return std::pow(b, double(x));
}
}
int main()
{
// A constant-expression context
constexpr double kilo = power(10.0, 3);
int n = 3;
// Not a constant expression, because n cannot be
// converted to an rvalue
// in a constant-expression context
// Equivalent to std::pow(10.0, double(n))
double mucho = power(10.0, n);
std::cout << kilo << " " << mucho << "\n"; // (3)
}
Die Funktion power
ist constexpr
. Das bedeutet, dass sie zur Compilezeit ausgefĂĽhrt werden kann. Der erste Aufruf von power
bewirkt eine AusfĂĽhrung zur Compilezeit, weil das Ergebnis zur Compilezeit angefordert wird: constexpr double kilo = power(10.0, 1).
Der zweite Aufruf hingegen kann nur zur Laufzeit ausgefĂĽhrt werden, weil das Funktionsargument n
kein konstanter Ausdruck ist: double mucho = power(10.0, n).
Dank std::is_constant_evaluated
wird zur Compilezeit und zur Laufzeit ein unterschiedlicher Code ausgefĂĽhrt. Zur Compilezeit wird der if-
Zweig ausgefĂĽhrt und zur Laufzeit der else
-Zweig. Beide power
Aufrufe ergeben 1000.
Eine immediate-Funktion ist eine consteval
-Funktion. Eine consteval
-Funktion ist eine Funktion, die nur zur Compilezeit ausgefĂĽhrt werden kann. Mehr ĂĽber consteval
-Funktionen steht in meinem C++20 Beitrag: Zwei neue Schlüsselwörter in C++20: consteval und constinit.
Basierend auf consteval if
lässt sich std::is_constant_evaluated
implementieren:
constexpr bool is_constant_evaluated() {
if consteval {
return true;
} else {
return false;
}
}
auto(x)
and auto{y}
Ein generischer Weg, um eine Kopie eines Objekts in C++ zu erhalten, ist auto copy = x
;. Dies funktioniert, hat aber ein Problem: copy
ist ein lvalue, aber manchmal will man einen prvalue. prvalue ist die AbkĂĽrzung fĂĽr pure rvalue. Ein pure rvalue ist ein Ausdruck, dessen Auswertung ein Objekt initialisiert. Mehr ĂĽber Wertkategorien steht in Barrys Beitrag: Value Categories in C++17.
Die Aufrufe auto(x)
und auto{x}
wandeln x in einen prvalue so um, als ob sie x
als Funktionsargument per value ĂĽbergeben wĂĽrden. auto(x)
und auto{x}
fĂĽhren eine decay copy durch.
In meinen Schulungen wird mir oft die Frage gestellt, was decay bedeutet. Deshalb möchte ich es näher erläutern. Decay bedeutet im Wesentlichen, dass einige Typinformationen beim Kopieren eines Werts verloren gehen. Ein typisches Beispiel ist eine Funktion, die ihr Argument als Wert annimmt. Hier sind die verschiedenen Arten von decay:
- Array-zu-Zeiger-Konvertierung
- Funktion-zu-Zeiger-Konvertierung
- Verwerfen von
const/volatile
Qualifiern - Entfernen von Referenzen
Das folgende Programm zeigt die vier Arten des decays (Zerfalls):
// decay.cpp
void decay(int*, void(*)(int), int, int ) { } // (5)
void func(int){} // (2)
int main() {
int intArray[5]{1, 2, 3, 4, 5}; // (1)
const int myInt{5}; // (3)
const int& myIntRef = myInt; // (4)
decay(intArray, func, myInt, myIntRef);
}
Die Funktion decay
(5) benötigt einen Zeiger auf einen int
, einen Funktionszeiger und zwei int
s. Das erste Argument des Funktionsaufrufs ist ein int
-Array (1), das zweite eine Funktion (2), das dritte ein const int
(3) und das letzte ist ein const int&
(4).
Die type-traits Bibliothek verfĂĽgt ĂĽber die Funktion std::decay. Mit dieser Funktion kann man diesen decay direkt auf einen Typ anwenden. Dementsprechend sind dies die entsprechenden Typkonvertierungen mit std::decay
.
// decayType.cpp
#include <type_traits>
int main() {
// (1)
// int[5] -> int*
static_assert(std::is_same<std::decay<int[5]>::type,
int*>::value);
// (2)
// void(int) -> void(*)(int)
static_assert(std::is_same<std::decay<void(int)>::type,
void(*)(int)>::value);
// (3)
// const int -> int
static_assert(std::is_same<std::decay<const int>::type,
int>::value);
// (4)
// const int& -> int
static_assert(std::is_same<std::decay<const int&>::type,
int>::value);
}
Wie geht's weiter?
Es gibt noch weitere kleine Perlen in C++23. In meinem nächsten Artikel werde ich meine Reise mit weiteren Kernsprachenfeatures von C++23 fortsetzen. (rme)