New Attributes with C++20
With C++20, we got new and improved attributes such as [[nodiscard(“reason”)]], [[likely]], [[unlikely]], and [[no_unique_address]]. In particular, [[nodiscard(“reason”)]] allows it to express the intention of your interface way clearer.
Attributes allow it to express declaratively the intention of your code.
New Attributes
During the writing of this article, I become a big fan of [[nodiscard(“reason”)]]. Consequently, I want to start with my favorite.
[[nodiscard(“reason”)]]
We have [[nodiscard]] already since C++17. C++20 added the possibility to add a message to the attribute. Unfortunately, I ignored [[nodiscard]] in the last few years. Let me present it now. Imagine, I have the following program.
// 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) }
Thanks to perfect forwarding and parameter packs, the factory function create can call any constructor and return a heap-allocated object.
The program has many issues. First, line (1) has a memory leak because the heap created int is never destroyed. Second, the error code of the function errorPronceFunction (2) is not checked. Last, the constructor call MyType(5, true) creates a temporary, which is created and immediately destroyed. This is at least a waste of resources. Now, [[nodiscard]] comes into play.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
[[nodiscard]] can be used in a function, enumeration, or class declaration. If you discard the return value from a function declared as nodiscard, the compiler should issue a warning. The same holds for a function returning by copy an enumeration or a class declared as [[nodiscard]]. A cast-to-void should not emit a warning.
Let me see what this means. In the following example, I use the C++17 syntax of the attribute [[nodiscard]].
// 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) }
The factory function create and the enum ErrorCode is declared as [[nodiscard]]. Consequently, calls (1) and (2) create a warning.
Way better, but the program still has a few issues. [[nodiscard]] cannot be used for functions such as a constructor returning nothing. Therefore, the temporary MyType(5, true) is still created without warning. Second, the error messages are too general. As a user of the functions, I want a reason why discarding the result is an issue.
Both issues can be solved with C++20. Constructors can be declared as [[nodiscard]], and the warning could have additional information.
// 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) }
Now, the user of the functions gets a specific message. Here is the output of the Microsoft compiler.
By the way, many existing functions in C++ could benefit from the [[nodiscard]] attribute. For example, when you don’t use the return value of std::asnyc, an asynchronously meant std::async call becomes implicitly synchronous. What should run in a separate thread behaves as a blocking function call. Read more about the counterintuitive behavior of std::async in my blog “The Special Futures“.
While studying the [[nodiscard]] syntax on cppreference.com, I noticed that the overload of std::async changed with C++20. Here is one:
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 as return-type of the promise std::async is declared as [[nodiscard]].
The following two attributes [[likely]] and [[unlikely]], are about optimization.
[[likely]] and [[unlikely]]
The proposal P0479R5 for likely and unlikely attributes is the shortest proposal I know of. To give you an idea, this is an interesting note to the proposal. “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.”
To make it short, both attributes allow it to give the optimizer a hint about which path of execution is more or less likely.
for(size_t i=0; i < v.size(); ++i){ if (v[i] < 0) [[likely]] sum -= sqrt(-v[i]); else sum += sqrt(v[i]); }
The story with optimization goes on with the new attribute [[no_unique_address]]. This time the optimization addresses space.
[[no_unique_address]]
[[no_unique_address]] expresses that this data member of a class need not have an address distinct from all other non-static data members of its class. Consequently, if the member has an empty type, the compiler can optimize it to occupy no memory.
The following program exemplifies the usage of the new attribute.
// 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; }
The class NoUniqueAddress has another size as an int (2) but not the class UniqueAddress (3). The members d and e of NoUniqueAddress (4) have different addresses but not the members of the class UniqueAddress (5).
What’s next?
The volatile qualifier is one of the darkest corners in C++. Consequently, most of volatile has been deprecated in C++20.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!