Standard-Attribute für C++

C++ Attribute – Clean Code für den Compiler

«Guter Code dokumentiert sich selbst.» Wir Programmierer lieben diesen Satz und mit den seit C++11 verfügbaren Standard-Attributen kommen wir diesem Wunschdenken einen Schritt näher.

08.03.2018Text: tnt-graphics0 Kommentare
Softwareentwicklung Mann

Die ersten Standard-Attribute noreturn und carries_dependency wurden bereits mit C++11 eingeführt. C++14 brachte uns das allseits beliebte deprecated und mit C++17 sind nun auch fallthrough, nodiscard und maybe_unused dazu gekommen.

In C++11 wurde mit der [[]]-Notation eine Antwort auf den Wildwuchs eingeführt, der aus all den #pragma, __attribute__(), __declspec und weiteren compiler-spezifischen Varianten entstand. Dies ermöglicht es uns, ganz im Sinne von Clean Code, eine bestimmte Absicht direkt für den Programmierer, die statische Codeanalyse und den Compiler zu dokumentieren.

[[noreturn]] (C++11)

Es sollte nicht die Regel sein, aber es gibt Fälle, in denen Funktionen nicht ordentlich zurückkehren. Zum Beispiel bei einem exit() Call, oder wenn auf jeden Fall eine Exception geworfen wird. In diesem Fall kann die Methode mit [[noreturn]] dekoriert werden und gibt so den Hinweis, dass es sich hier um eine spezielle Methode handelt.  Aber Achtung: sollte eine so dekorierte Funktion tatsächlich mit einem return zurückkehren, ist das Verhalten nicht definiert.

[sourcecode lang=“cpp“]
[[noreturn]]void always_throw() { throw 123; };
[/sourcecode]

[[deprecated]] (C++14)

Während dem Lebenszyklus einer Software kommt es immer wieder vor, dass Teile des Codes nicht mehr aktuell sind oder durch bessere Versionen ersetzt werden. Mit dem Attribut [[deprecated]] können solche Codeabschnitte markiert werden. Nicht nur Funktionen und Klassen, sondern auch Namespaces, Template-Spezialisierungen und Enums können so markiert werden. Seit C++17 kann zusätzlich ein Grund oder ein Hinweis für eine allfällige Migration hinzugefügt werden. Wird auf diese Art markierter Code verwendet, hat dies eine Warnung beim Kompilieren zur Folge.

[sourcecode lang=“cpp“]
// Deprecation with a reason
[[deprecated(„black magic is evil“)]] void black_magic(){};

// generates a compiler warning if called
[[deprecated]] void ancient_magic() {}
[/sourcecode]

 

[[fallthrough]] (C++17)

Wenn auch normalerweise ungern gesehen, kommt es doch in gewissen Fällen vor, dass wir in einem switch-case-Statement die Codeteile des darauffolgenden case-Blocks mit ausführen wollen. Im folgenden Code-Stück soll als Beispiel im Fall ‘b’ auch der Code von ‘c’ mit ausgeführt werden.

[sourcecode lang=“cpp“]
switch (c) {
case ‚a‘:
f();
break;
case ‚b‘:
g();
[[fallthrough]];
case ‚c‘:
h();
break;
case ‚d‘: //no [[fallthrough]] necessary
case ‚e‘:
e();
break;
}
[/sourcecode]

 

Mit dem Attribut [[fallthrough]] können wir unseren Kollegen und der statischen Codeanalyse explizit deutlich machen, dass genau dies unsere Absicht war. Hätten wir andererseits für den Fall ‘a’ das break vergessen, kann uns nun der Compiler auf den Fehler aufmerksam machen. In GCC 7 ist dies mit der Option -Wimplicit-fallthrough beziehungsweise mit -Wextra möglich. Leere case-Statements, wie hier im Fall ‘d’ (ohne break), produzieren weiterhin auch ohne [[fallthrough]] keine Warnung.

 

[[nodiscard]] (C++17)

Dies ist ebenfalls ein sehr hilfreiches Attribut für den täglichen Gebrauch. Wie oft haben wir schon Aufrufe von Funktionen gesehen, die einen bool oder einen int als Statuscode zurückgeben – der dann geflissentlich ignoriert wird. Mit [[nodiscard]] können wir solche Situationen endlich verhindern. Und es kommt noch besser: mit [[nodiscard]] kann dies gleich für einen ganzen Datentyp gesetzt werden. So kann ein Library-Entwickler den Benutzer mit Nachdruck auffordern gewisse Arten von Rückgabewerten weiter zu behandeln.

[sourcecode lang=“cpp“]
struct[[nodiscard]] demon{}; // Demons need to be kept and named struct ghost {};

demon summon_demon() { return demon(); }

// summoned ghosts need to be kept [[nodiscard]] ghost summon_ghost() { return ghost(); }

void summon() {
auto d = summon_demon(); // OK
auto g = summon_ghost(); // OK
// Compiler Warning, because returned demon is not kept summon_demon();

// Compiler Warning, because the function of summoning is nodiscard summon_ghost();
}
[/sourcecode]

 

[[maybe_unused]] (C++17)

Wer kennt es nicht: Wir möchten im Debug-Mode einen erweiterten Assert in eine Funktion einbauen, z.b. um Design by Contract zu realisieren. Das passt aber irgendwie nicht ohne eine zusätzliche Variable. Nun schimpft der Compiler im Release-Mode – wo wir den Assert nicht mehr rein kompilieren, dass die Variable nicht mehr benutzt wird. Seit C++17 können wir das mit [[maybe_unused]] zusätzlich markieren. Das Attribut beinflusst nur die Ausgabe von Compiler-Warnungen. Wenn eine Variable [[maybe_unused]] deklariert wird, kann sie vom Compiler nach wie vor weg optimiert werden, falls sie nicht gebraucht wird.

[sourcecode lang=“cpp“]
[[maybe_unused]] void f([[maybe_unused]] bool thing1,
[[maybe_unused]] bool thing2) {
[[maybe_unused]] bool b = thing1 && thing2;

assert(b); // if assert is compiled out and b is unused

assert(thing1 &&
thing2); // parameters thing1 and thing2 are not used, no warning
}
[/sourcecode]

 

[[carries_dependency]] (C++11)

Carries dependency gibt dem Benutzer und dem Compiler einen Hinweis, dass Speicher im Zusammenhang mit atomics “transparent” behandelt werden kann.

[sourcecode lang=“cpp“]
void opaque_func(int *p){/* do something with p */};​

[[carries_dependency]] void transparent_func(int *p) {
/* do something with p */
}

void illustrate_carries_dependency() {
std::atomic<int *> p;
int *atomic = p.load(std::memory_order_consume);

if (atomic)
std::cout << *atomic << std::endl; // transparent for the the compiler

if (atomic) opaque_func(atomic); // if from another compile unit and not inline, the // compiler might construct a memory fence here

if (atomic)
transparent_func(atomic); // marked as to work in the same memory-dependency // tree, compiler can omit the memory fence
}
[/sourcecode]

 

Nebst der erweiterten Dokumentation sind vor allem Standard-Attribute ein weiterer Schritt in Richtung: “Weg mit den Dialekten – Ein C++ für alle Compiler und Plattformen”. Dies ist ein guter Weg der mit C++11/14 begonnen wurde und mit C++17 weiter voranschreitet. Wir sind gespannt auf die kommenden Änderungen in C++20…

Unser Wissen im Abo

Menschen bei bbv

«Gute Ideen sind unser Rohstoff»

Agile Software Development
Technica Radar 2024

Mehr als KI: Das sind die wichtigsten IT-Trends 2024

AI/KI
6 Tipps, wie Sie Ihr Team effizienter machen

Müde Augen ade: So helfen kurze Codezeilen

Agile

Attention!

Sorry, so far we got only content in English for this section.

Achtung!

Entschuldigung, bisher haben wir für diesen Abschnitt nur deutschsprachige Inhalte.