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.

In Pocket speichern vorlesen Druckansicht 19 Kommentare lesen

(Bild: Shutterstock.com/Kenishirotie)

Lesezeit: 4 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Die Kernsprache C++23 hat mehr zu bieten als Deducing This. Heute werde ich über die kleinen Perlen schreiben.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

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 verhält sich wie if (std::is_constant_evaluated()) { }, besitzt aber ein paar Vorteile:

  1. Es wird kein Header <type_traits> benötigt.
  2. Es besitzt eine einfachere Syntax als std::is_constant_evaluated.
  3. 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;
    }
}

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:

  1. Array-zu-Zeiger-Konvertierung
  2. Funktion-zu-Zeiger-Konvertierung
  3. Verwerfen von const/volatile Qualifiern
  4. 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 ints. 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);          

}

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)