zurück zum Artikel

Neue Attribute mit C++20

Rainer Grimm

Mit C++20 gibt es neue und verbesserte Attribute: [[nodiscard("reason")]], [[likely]], [[unlikely]] und [[no_unique_address]]. Insbesondere [[nodiscard("reason")]] erlaubt es, die Intention eines Interfaces deutlicher auf den Punkt zu bringen.

Mit C++20 erhalten wir die neuen und verbesserten Attribute [[nodiscard("reason")]], [[likely]], [[unlikely]] und [[no_unique_address]]. Insbesondere [[nodiscard("reason")]] erlaubt es, die Intention eines Interfaces deutlicher auf den Punkt zu bringen.

Neue Attribute mit C++20

Dank Attributen lĂ€sst sich die Absicht des Codes deklarativ ausdrĂŒcken.

WĂ€hrend des Schreibens dieses Artikels bin ich zu einem großen Fan von [[nodiscard("reason")]] geworden. Daher beginne ich damit. Bereits seit C++17 gibt es das Attribut [[nodiscard]]. Mit C++20 wurde es um die Möglichkeit erweitert, eine Nachricht hinzuzufĂŒgen. UnglĂŒcklicherweise habe ich [[nodiscard]] in den letzten Jahren ignoriert. Diese Scharte möchte ich jetzt auswetzen und mit dem folgenden Programm starten:

// withoutNodiscard.cpp

#include <utility>

struct MyType {

MyType(int, bool) {}

};

template <typename T, typename ... Args>
T* create(Args&& ... args){
return new T(std::forward<Args>(args)...);
}

enum class ErrorCode {
Okay,
Warning,
Critical,
Fatal
};

ErrorCode errorProneFunction() { return ErrorCode::Fatal; }

int main() {

int* val = create<int>(5);
delete val;

create<int>(5); // (1)

errorProneFunction(); // (2)

MyType(5, true); // (3)

}

Dank Perfect Forwarding und Parameter Packs erlaubt es die Fabrikfunktion create, jeden Konstruktor aufzurufen und ein Heap-allokiertes Objekt zurĂŒckzugeben.

Das Programm hat einige UnzulĂ€nglichkeiten. Zuerst einmal verursacht die Zeile (1) ein Speicherleck, denn das auf dem Heap erzeugte Objekt wird nicht destruiert. DarĂŒber hinaus wird der Fehlercode der Funktion errorProneFunction (2) nicht geprĂŒft. Zuletzt erzeugt der Konstruktoraufruf MyType(5, true) eine temporĂ€re Variable, die sofort wieder gelöscht wird. Das ist zumindest Verschwendung von Ressouren.

Nun kommt aber [[nodiscard]] ins Spiel. Es lĂ€sst sich in Funktions-, AufzĂ€hler- und Klassendeklaration verwenden. Der Compiler soll eine Warnung ausgeben, falls du den RĂŒckgabewert einer als "nodiscard" deklarierten Funktion ignorierst, die ihren Wert per Copy zurĂŒckgibt. Dasselbe gilt fĂŒr eine Funktion, die eine als "nodiscard" erklĂ€rte AufzĂ€hlung oder eine Klasse per Copy zurĂŒckgibt. Das gilt aber nicht, falls eine Konvertierung nach void angewandt wird.

Was heißt das nun? Im folgenden Beispiel setze ich die C++17-Synax des Attributes [[nodiscard]] ein:

// nodiscard.cpp

#include <utility>

struct MyType {

MyType(int, bool) {}

};

template <typename T, typename ... Args>
[[nodiscard]]
T* create(Args&& ... args){
return new T(std::forward<Args>(args)...);
}

enum class [[nodiscard]] ErrorCode {
Okay,
Warning,
Critical,
Fatal
};

ErrorCode errorProneFunction() { return ErrorCode::Fatal; }

int main() {

int* val = create<int>(5);
delete val;

create<int>(5); // (1)

errorProneFunction(); // (2)

MyType(5, true); // (3)

}

Die Fabrikfunktion create und die enum ErrorCode sind als [[nodiscard]] deklariert. Konsequenterweise erzeugen die Aufrufe (1) und (2) eine Warnung.

Neue Attribute mit C++20

Das ist schon deutlich besser, einige UnzulĂ€nglichkeiten bestehen aber immer noch. [[nodiscard]] lĂ€sst sich nicht auf Konstruktoren, die natĂŒrlich nichts zurĂŒckgeben, anwenden. Daher wird der temporĂ€re Wert MyType(5, true) ohne Warnung erzeugt. DarĂŒber hinaus sind mir die Fehlermeldungen zu allgemein. Als Anwender der Funktionen möchte ich wissen, warum das Verwerfen des Werts ein Problem darstellt.

Beide UnzulĂ€nglichkeiten lassen sich mit C++20 lösen. Konstruktoren können als [[nodiscard]] deklariert werden und der Warnung lĂ€sst sich eine zusĂ€tzliche Nachricht hinzufĂŒgen:

// nodiscardString.cpp

#include <utility>

struct MyType {

[[nodiscard("Implicit destroying of temporary MyInt.")]] MyType(int, bool) {}

};

template <typename T, typename ... Args>
[[nodiscard("You have a memory leak.")]]
T* create(Args&& ... args){
return new T(std::forward<Args>(args)...);
}

enum class [[nodiscard("Don't ignore the error code.")]] ErrorCode {
Okay,
Warning,
Critical,
Fatal
};

ErrorCode errorProneFunction() { return ErrorCode::Fatal; }

int main() {

int* val = create<int>(5);
delete val;

create<int>(5); // (1)

errorProneFunction(); // (2)

MyType(5, true); // (3)

}

Nun erhalten Anwender der Funktion eine spezifische Warnung. Hier ist die Ausgabe des Microsoft-Compilers:

Neue Attribute mit C++20

Viele Funktionen in C++ können vom [[nodiscard]]-Attribut profitieren. Wenn du zum Beispiel den RĂŒckgabewert von std::async nicht verwendest, wird aus einem asynchronen Aufruf ein synchroner. Was in einem separaten Thread ausgefĂŒhrt werden soll, wird daher zu einem blockierenden Funktionsaufruf. Mehr Details zum ĂŒberraschenden Verhalten von std::async bietet mein Artikel "Besondere Futures mit std::async [1]".

Bei meiner Recherche zur [[nodiscard]]-Syntax auf cppreference.com fiel mir auf, dass die Überladungen von std::async [2] mit C++20 verĂ€ndert wurden. Hier ist exemplarisch eine der Überladungen:

template< class Function, class... Args>
[[nodiscard]]
std::future<std::invoke_result_t<std::decay_t<Function>,
std::decay_t<Args>...>>
async( Function&& f, Args&&... args );

std::future als RĂŒckgabetyp des Promise std::async ist als [[nodiscard]] deklariert.

Die nÀchsten zwei neue Attribute [[likely]] und [[unlikely]] beschÀftigen sich mit der Optimierung.

Das Proposal P0479R5 zu "likelyl" und "unlikey" ist das kĂŒrzeste, das ich kenne. Es besteht fast nur aus einer Anmerkung, die ich zitieren möchte: "The use of the likely attribute is intended to allow implementations to optimize for the case where paths of execution including it are arbitrarily more likely than any alternative path of execution that does not include such an attribute on a statement or label. The use of the unlikely attribute is intended to allow implementations to optimize for the case where paths of execution including it are arbitrarily more unlikely than any alternative path of execution that does not include such an attribute on a statement or label. A path of execution includes a label if and only if it contains a jump to that label. Excessive usage of either of these attributes is liable to result in performance degradation."

Beide Attribute erlauben es, dem Compiler einen Hinweis zu geben, welcher AusfĂŒhrungspfad mit höherer Wahrscheinlichkeit ausgefĂŒhrt wird:

for(size_t i=0; i < v.size(); ++i){
if (v[i] < 0) [[likely]] sum -= sqrt(-v[i]);
else sum += sqrt(v[i]);
}

Die Geschichte zur Optimierung mit den neuen Attributen endet hier aber noch nicht. Dank [[no_unique_address]] lÀsst sich der Addressraum optimieren.

[[no_unique_address]] drĂŒckt aus, dass dieses Mitglied einer Klasse keine Adresse benötigt, die sich von allen anderen nichtstatischen Mitgliedern der Klasse unterscheidet. Falls dieses Mitglied ein Empty Type ist, kann der Compiler konsequenterweise seinen Speicherplatz wegoptimieren.

Das folgende Beispiel stellt [[no_unique_address]] genauer vor:

// uniqueAddress.cpp

#include <iostream>

struct Empty {};

struct NoUniqueAddress {
int d{};
Empty e{};
};

struct UniqueAddress {
int d{};
[[no_unique_address]] Empty e{}; // (1)
};

int main() {

std::cout << std::endl;

std::cout << std::boolalpha;

std::cout << "sizeof(int) == sizeof(NoUniqueAddress): " // (2)
<< (sizeof(int) == sizeof(NoUniqueAddress)) << std::endl;

std::cout << "sizeof(int) == sizeof(UniqueAddress): " // (3)
<< (sizeof(int) == sizeof(UniqueAddress)) << std::endl;

std::cout << std::endl;

NoUniqueAddress NoUnique;

std::cout << "&NoUnique.d: " << &NoUnique.d << std::endl; // (4)
std::cout << "&NoUnique.e: " << &NoUnique.e << std::endl; // (4)

std::cout << std::endl;

UniqueAddress unique;

std::cout << "&unique.d: " << &unique.d << std::endl; // (5)
std::cout << "&unique.e: " << &unique.e << std::endl; // (5)

std::cout << std::endl;

}

(1) wendet das neue Attribut [[no_unique_address]] an. Die GrĂ¶ĂŸe der Klasse NoUniqueAddress unterscheidet sich vom Datentyp int (2). Das gilt aber nicht fĂŒr die Klasse UniqueAddress (3). Die Mitglieder d und e der Klasse NoUniqueAddress (4) besitzen verschiedene Adressen. Das gilt wiederum nicht fĂŒr die Mitglieder der Klasse UniqueAddress (5).

Neue Attribute mit C++20

Der volatile-Spezifizier steht fĂŒr eines der dunkelsten Ecken in C++. Daher wird seine Semantik in C++20 deutlich eingeschrĂ€nkt. ( [3])


URL dieses Artikels:
https://www.heise.de/-4871578

Links in diesem Artikel:
[1] https://www.grimm-jaud.de/index.php/blog/std-async-warten-im-destruktor
[2] https://en.cppreference.com/w/cpp/thread/async
[3] mailto:rainer@grimm-jaud.de