New Attributes with C++20

Contents[Show]

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.

 TimelineCpp20CoreLanguage

Attributes allow it to express declarative 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 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 all any constructor and return a heap-allocated object.

The program has many issues. First, line (1) has a memory leak, because the on 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.

[[nodiscard]] can be used in a function declaration, enumeration declaration, 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 are declared as [[nodiscard]]. Consequently, the calls (1) and (2) create a warning.

nodiscard

 

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 a warning. Second, the error messages are too general. As a user of the functions, I want to have 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.

nodiscardString2

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 synchronously. What should run in 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 next 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 the 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, 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).

 

uniqueAddress

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: Meeting C++, Matt Braun, Roman Postanciuc, Venkata Ramesh Gudpati, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang Gärtner, Jon Hess, Christian Wittenhorst, Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Lakshman, Kuchlong Kuchlong, Avi Kohn, Serhy Pyton, Robert Blanch, Kuma [], Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, and Friedrich Huber.

 

 

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.

Standard Seminars 

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

Contact Me

Modernes C++,

RainerGrimmSmall

My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 6495

Yesterday 10139

Week 37536

Month 6495

All 4627389

Currently are 201 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments