DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on

Attributes in C23 and C++

Introduction

An attribute in either a C or C++ program is a little bit of extra helpful information attached to one of a declaration, statement, or function, that neither compilers nor humans can either know or intuit from just looking at the code, but can be used to help compilers do a better job of either diagnostics or optimization.

C++11 introduced a new syntax for attributes that was later adopted into C23. The full syntax is a bit baroque, but the basic syntax is simply:

[[ attribute-list ]]

that is a sequence of one or more attributes separated by commas enclosed between double square brackets where an attribute is simply an identifier. For example, the standard library function exit() is now declared as:

[[noreturn]] void exit( int status );
Enter fullscreen mode Exit fullscreen mode

which tells both compilers and humans that the function never returns — something the compiler can’t intuit from the name alone. (Of course, humans can intuit that in this case, but not in all cases; and the idea is primarily to help the compiler in this case.)

Prior to C23 or C++11, the only way to attach attributes was using compiler-specific syntax such as __attribute__ for gcc and clang, or __declspec for MSVC.

Historically, C11 actually introduced attributes, but as keywords — well, an attribute (singular): _Noreturn. Hence, exit() used to be declared as:

_Noreturn void exit( int status );
Enter fullscreen mode Exit fullscreen mode

Why? The C Committee is fairly conservative. My guess is, at the time, they didn’t want to introduce a whole new syntax. However, keywords for attributes are problematic in that:

  1. If a particular attribute isn’t supported by a particular compiler, the compiler has no choice but to assume it’s a syntax error.

  2. It offers no mechanism for compiler vendors to add their own attributes without causing code using them to break on other compilers (due to problem 1).

The [[...]] syntax is simply better because the compiler knows it’s an attribute declaration. If a particular attribute isn’t supported, the compiler will simply ignore it (or at most warn if you enable warning about unsupported attributes), hence your code will still compile.

In C23, the C Committee saw the light an adopted the [[...]] syntax deprecating the keyword syntax. Obviously, the C and C++ Committees are separate and adopt things at different rates. Despite that, they do try to maximize compatibility between the two languages (eventually). Currently, there’s a common subset of attributes that are supported by both C23 and C++, but also attributes that are supported by only one or the other.

Attribute Placement

In general, attributes can be inserted almost anywhere in a program: before or after declarations or statements. Where they’re inserted influences what they’re an attribute of. For example, when at attribute is placed before a declaration, such as in:

[[maybe_unused]] int debug_open_count, debug_close_count;
Enter fullscreen mode Exit fullscreen mode

it applies to all of the things being declared; in contrast, when it’s applied after a declaration, such as in:

int x, debug_count [[maybe_unused]], y;
Enter fullscreen mode Exit fullscreen mode

it applies only to the variable to its immediate left.

Note that inserting an attribute after a declaration has started (above, by int) but before a particular thing being declared is an error:

int x, [[maybe_unused]] debug_count, y;  // error
Enter fullscreen mode Exit fullscreen mode

Structure, union, and enumeration declarations can also have attributes, for example:

struct [[maybe_unused]] debug_info {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

but the attribute must go where shown; putting an attribute either before or after is an error.

Why weren’t such placements allowed? I don’t know. My only guess is that they make parsing harder.

Standard Attributes

Some attributes can also take arguments between parentheses as we’ll see.

[[assume]]

Languages: Not in C; C++23.
Syntax: [[assume(expression)]]
Attribute of: Null statements only.
For: Speed optimization.

The assume attribute tells the compiler to assume that the given expression is true. Consequently, it may be able to generate more efficient code. For example:

[[assume( n != 0 && (n & (n - 1)) == 0 )]];
Enter fullscreen mode Exit fullscreen mode

tells the compiler to assume than n is a power of two. Given that, the compiler will likely be able to optimize better, e.g., substitute cheaper shifts for more expensive multiplications or divisions. Note that the expression is not evaluated.

What happens if the assumption turns out to be false at run-time? It results in undefined behavior. Because this is so, you should:

  • Use assume sparingly, typically only when squeezing out a bit more performance where it really matters, e.g., in tight loops or functions that are called frequently. (You should have profiled your code both with and without assume to see if it actually makes a significant difference.)

  • Be really, really sure that the assumption is always true.

One way to make using assume safer is always to pair it with assert, for example:

assert( n > 0 );
[[assume( n > 0 )]];
Enter fullscreen mode Exit fullscreen mode

In a debug build, the assert will check your assumption at run-time; the assume will provide for better optimization. If your program passes your test suite, then you can disable assertions for a production build yet still benefit from better optimized code.

[[deprecated]]

Languages: C23; C++14.
Syntax: [[deprecated]], [[deprecated("reason")]]
Attribute of: Anything.
For: Diagnostics.

This attribute should be fairly obvious in that it marks the thing it’s attached to as deprecated such that if the thing is used in the program, the compiler will generate a warning — meaning you shouldn’t use it because it’s presumably going to be removed eventually.

If reason (an arbitrary string literal) is given, the compiler will include that in the warning message. Typically, it’s the reason the thing was deprecated or what you should use instead. For example:

[[deprecated("Use get_user_id instead")]]
int get_user( char const *name );
Enter fullscreen mode Exit fullscreen mode

[[fallthrough]]

Languages: C23; C++17.
Syntax: [[fallthrough]]
Attribute of: Null statements only (at end of case or default).
For: Diagnostics.

As you know, cases fall through into the next case unless you break (or continue, return, or longjmp). You can request that the compiler warn you if you forget. However, if you really want to fall through, you can use fallthrough to suppress the warning, for example:

switch ( argc ) {
  case 2:
    if ( !freopen( fout_path, "w", stdout ) )
      file_error( fout_path );
    [[fallthrough]];
  case 1:
    if ( !freopen( fin_path, "r", stdin ) )
      file_error( fin_path );
}
Enter fullscreen mode Exit fullscreen mode

[[indeterminate]]

Languages: Not in C; C++26.
Syntax: [[indeterminate]]
Attribute of: Block variable or function parameter.
For: Speed optimization.

Before indeterminate can be explained, you may need to be reminded that, in both C and C++, an uninitialized variable’s value is indeterminate; reading such a variable results in undefined behavior, for example:

int main() {
  int x;                // indeterminate value
  printf( "%d\n", x );  // undefined behavior
}
Enter fullscreen mode Exit fullscreen mode

C++26 changed this such that an uninitialized variable’s value is merely erroneous — which means the value you get is “garbage,” but reading such a variable does not result in undefined behavior. This is a good thing because such a common bug will be easier to debug.

A variable marked indeterminate restores the pre-C++26 behavior of the variable’s value start off as indeterminate, for example:

[[indeterminate]] int x;
Enter fullscreen mode Exit fullscreen mode

Why would you want to do that? In rare cases, treating a variable as indeterminate will yield slightly better performance. Since the attribute restores undefined behavior, you’d better know what you’re doing.

[[likely]] & [[unlikely]]

Languages: Not in C; C++20.
Syntax: [[likely]], [[unlikely]]
Attribute of: if, else, case.
For: Speed optimization.

An if, else, or a case can be marked either likely or unlikely if it’s either of those. This allows the compiler to generate better branch-prediction code, for example:

if ( n > 0 ) [[likely]]
  // ...
Enter fullscreen mode Exit fullscreen mode

or:

switch ( err ) {
  [[likely]] case ERR_NONE:
    return;
  // ...
}
Enter fullscreen mode Exit fullscreen mode

For either an if or else, the attribute goes after; for a case, it goes before. (Why the inconsistency? I don’t know.)

Note that “likely” and “unlikely” mean something like 99% of the time, not 51%. Hence, you should mark something likely or unlikely only if it’s very likely or unlikely.

Use of either also helps humans to understand that a condition is either.

[[maybe_unused]]

Languages: C23; C++17.
Syntax: [[maybe_unused]]
Attribute of: Anything.
For: Diagnostics.

A variable or function marked maybe_unused tells the compiler not to warn that the variable or function is unused. This can be useful when checking the result of a function with an assert:

[[maybe_unused]] int rv = f();
assert( rv == 0 );
Enter fullscreen mode Exit fullscreen mode

When compiled with NDEBUG defined, the assert and use of rv will disappear. If that was the only use of rv, then the compiler would warn you. Marking rv as maybe_unused would suppress the warning.

[[nodiscard]]

Languages: C23; C++17.
Syntax: [[nodiscard]], [[nodiscard("reason")]] (C++20)
Attribute of: Functions only.
For: Diagnostics.

A function marked nodiscard such as:

[[nodiscard]] int initialize();
Enter fullscreen mode Exit fullscreen mode

when called where you discard its return value such as:

int main( int argc, char const *argv[] ) {
  // ...
  initialize();
  // ...
}
Enter fullscreen mode Exit fullscreen mode

will cause the compiler warn you:

warning: ignoring return value of function 'nodiscard'
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can declare a structure, union, or enumeration with nodiscard such as:

enum [[nodiscard]] error_code {
  ERROR_NONE,
  ERROR_USAGE,
  // ...
};
Enter fullscreen mode Exit fullscreen mode

Then whenever any function returns a value of error_code, it’s nodiscard implicitly.

I strongly encourage you to annotate every non-void function with nodiscard and omit it only for functions where the return value can safely be discarded.

In hindsight, every function should have been implicitly nodiscard and there should have been an ok_discard attribute instead to indicate that it’s OK to discard a return value. Making such a change now to either C or C++ would causes warnings for practically every program in existence.

[[noreturn]]

Languages: C23; C++11.
Syntax: [[noreturn]]
Attribute of: Functions only.
For: Diagnostics.

As mentioned previously, noreturn is used for functions that never return. You should always use it for such functions.

What happens if a noreturn function returns anyway? You guessed it: undefined behavior.

[[no_unique_address]]

Languages: Not in C; C++20.
Syntax: [[no_unique_address]]
Attribute of: Non-static, non-bit-field members of structures or classes.
For: Space optimization.

Before no_unique_address can be explained, you may need to be reminded that, in C++, although a class T can be declared with no non-static data members, sizeof(T) must be > 0, for example:

struct my_alloc {
  void* operator new( std::size_t size );
};
static_assert( sizeof(my_alloc) > 0 );
Enter fullscreen mode Exit fullscreen mode

However, when an object of such a class is a non-static data member of another class, it can be marked no_unique_address to allow the compiler to give it no size:

template<typename T, typename Alloc>
class my_container {
  // ...
private:
  [[no_unique_address]] Alloc _alloc;  // no space used
  // ...
};
Enter fullscreen mode Exit fullscreen mode

FYI, standard C doesn’t allow empty structures, so no special rule is needed to make its size > 0. That means no_unique_address couldn’t ever be adopted into C unless that rule changes first, which is unlikely.

[[reproducible]]

Languages: C23; not in C++.
Syntax: [[reproducible]]
Attribute of: Functions only.
For: Speed optimization.

A reproducible function is one that, given the same values for arguments (if any), will always return the same result. For example, given:

[[reproducible]] double sqrt( double x );
Enter fullscreen mode Exit fullscreen mode

and a call passing a literal or a constant allows the compiler to elide calls:

double a[2][2] = {
  { sqrt(2),       0 },
  {       0, sqrt(2) }
};
Enter fullscreen mode Exit fullscreen mode

Because the compiler now knows sqrt(2) always returns the same result, it can call the function only once and use the result twice. (Formally, this is idempotent.)

A reproducible function must also not cause side-effects aside from its internal state (if any) or parameters. (Formally, this is effectless.)

[[unsequenced]]

Languages: C23; not in C++.
Syntax: [[unsequenced]]
Attribute of: Functions only.
For: Speed optimization.

An unsequenced function is a superset of a reproducible one. Not only is the function one that, given the same values for arguments (if any), will always return the same result, it will do so regardless of when it’s called or in what order relative to everything else. Hence, sqrt should be declared as:

[[unsequenced]] double sqrt( double x );
Enter fullscreen mode Exit fullscreen mode

The compiler could decide to call sqrt(2) once at the start of the program and use its result thereafter.

An unsequenced function must also not maintain any internal state between calls that affects the result, i.e., no non-const, volatile, static, or thread_local variables. (Formally, this is stateless.)

Compiler-Specific Attributes

The attribute syntax also allows for attributes to have a prefix or namespace (even in C), for example:

[[gnu::nonnull(1)]]
Enter fullscreen mode Exit fullscreen mode

This allows for compiler-specific attributes since the standard will never include all of them.

Conclusion

Use of attributes improves both diagnostics and optimization of your programs. Where appropriate, should use them whenever possible.

Top comments (0)