C++ Attributes: nodiscard, deprecated, maybe_unused, likely, fallthrough and noreturn
Key takeaways
Standard C++ attributes give the compiler facts it cannot infer: a result must not be dropped, a function never returns, a case falls through on purpose. This guide shows the warnings each one produces and where each one quietly fails to help.
What attributes are for
Attributes, introduced in C++11 with the [[...]] syntax, are a standardized way to tell the compiler something about your code that it cannot deduce from the types alone. Before they existed, every compiler had its own spelling: __attribute__((warn_unused_result)) in GCC and Clang, __declspec(noreturn) or SAL annotations in MSVC. Libraries wrapped all of these in macros, and every project had a slightly different MY_NODISCARD header. The standard attributes replace most of those macros with one spelling that every conforming compiler accepts.
The important mental model is that the standard attributes are, with a couple of exceptions, hints and diagnostics, not semantics. A program that is correct with [[nodiscard]] is still correct if you delete it; you just lose a warning. The rules also say a compiler must ignore attributes it does not recognize. That is what makes them portable, but it has a side effect covered below: a misspelled attribute costs you a warning at most, not a compile error.
// Before: compiler-specific, wrapped in macros
#ifdef __GNUC__
__attribute__((warn_unused_result))
#endif
int compute();
// Standard since C++17
[[nodiscard]] int compute();
Where each attribute arrived:
- C++11:
[[noreturn]],[[carries_dependency]] - C++14:
[[deprecated]],[[deprecated("reason")]] - C++17:
[[fallthrough]],[[nodiscard]],[[maybe_unused]] - C++20:
[[likely]],[[unlikely]],[[no_unique_address]],[[nodiscard("reason")]]
All the warning text below comes from compiling the examples with g++ 10.3 using -std=c++20 -Wall -Wextra.
[[nodiscard]]
[[nodiscard]] on a function asks for a warning when a caller evaluates the call and throws the result away. Use it where ignoring the result is almost always a bug: error codes, bool success flags, newly allocated handles, and pure functions whose only effect is the value they return.
[[nodiscard]] int compute() { return 42; }
[[nodiscard("check the error code")]] int save() { return 0; }
int main() {
compute(); // warning
save(); // warning, with the reason string
int r = compute(); // fine
(void)compute(); // fine: explicit, visible discard
}
GCC reports:
warning: ignoring return value of 'int compute()', declared with attribute 'nodiscard' [-Wunused-result]
warning: ignoring return value of 'int save()', declared with attribute 'nodiscard': 'check the error code' [-Wunused-result]
The C++20 reason string is worth using. “Ignoring return value” tells a reader nothing; “check the error code” or “the returned handle owns the socket” tells them what they are about to break.
On a type instead of a function
Putting [[nodiscard]] on a class or enum makes every function that returns that type by value act as if it were marked:
struct [[nodiscard]] Result { bool ok; };
Result make();
Result& makeRef();
make(); // warning: ignoring returned value of type 'Result', declared with attribute 'nodiscard'
makeRef(); // no warning: returns a reference, not a Result value
This is the better choice for an error type such as Result, Status or expected<T, E>. You mark the type once instead of hoping every author of every function remembers the attribute. The reference case matters: the rule applies only to results returned by value, so an accessor that returns Result& does not get the warning.
Where nodiscard does not reach
I have seen teams mark an RAII type [[nodiscard]] expecting it to catch the classic “guard destroyed immediately” bug:
class [[nodiscard]] FileHandle { /* opens in ctor, closes in dtor */ };
FileHandle("data.txt"); // temporary: opened and closed on this line
auto file = FileHandle("data.txt"); // lives until end of scope
C++20 allowed compilers to warn when a constructor of a nodiscard type builds a temporary that is thrown away, but support varies. With g++ 10.3 the first line above compiles without any warning. For locks, the nastier version of this bug is std::unique_lock<std::mutex>(m);: it parses as a declaration of a new, default-constructed unique_lock named m that shadows the mutex, so it compiles and locks nothing. (The same line with lock_guard fails to compile only because lock_guard has no default constructor.) Name your guards, and do not treat [[nodiscard]] as protection against this pattern.
The other limitation is social. (void)f() and static_cast<void>(f()) silence the warning on purpose, and so does assigning to a variable nobody reads. The attribute only reports discards; it cannot tell whether a result was actually handled.
[[deprecated]]
[[deprecated]] marks a function, class, enumerator, alias or variable that still works but should no longer be used. Every use site gets a warning, and the declaration keeps working, which is exactly what you want during an API migration.
[[deprecated("use newFunc instead")]]
void oldFunc();
oldFunc();
// warning: 'void oldFunc()' is deprecated: use newFunc instead [-Wdeprecated-declarations]
Always include the replacement in the message. A bare [[deprecated]] produces “is deprecated” and leaves the caller searching the changelog.
A pattern that works well is to deprecate first, ship a release, then delete in the release after. Consumers who build with -Werror will feel the deprecation straight away, and that is one reason some libraries hide deprecation behind a macro users can switch off. If your own code has to keep calling the old API for a while, for example in a compatibility shim, suppress the warning locally with #pragma GCC diagnostic push / ignored "-Wdeprecated-declarations" / pop instead of turning the warning off for the whole build.
[[maybe_unused]]
[[maybe_unused]] suppresses unused-entity warnings for a variable, parameter, function or type that is only sometimes used. The typical case is a value only read in some build configurations:
void process(int value, [[maybe_unused]] int debugLevel) {
#ifdef DEBUG
log(debugLevel, value);
#endif
}
void g(int unused) {}
// without the attribute: warning: unused parameter 'unused' [-Wunused-parameter]
Two alternatives are worth knowing. For a parameter that is never used, such as a callback that must match a signature, you can simply leave the parameter unnamed: void onEvent(int /*code*/). For a variable that exists only for an assert, [[maybe_unused]] is the right tool, because under NDEBUG the assert expands to nothing and the variable really is unused in release builds.
[[noreturn]]
[[noreturn]] states that a function never returns to its caller: it always throws, calls std::exit/std::abort, or loops forever. The compiler uses this to skip “control reaches end of non-void function” warnings after calls to it, and to drop code that would run after the call.
[[noreturn]] void fatal(const char* msg) {
std::fprintf(stderr, "%s\n", msg);
std::exit(1);
}
int parse(const char* s) {
if (!s) fatal("null input"); // no "missing return" warning on this path
return std::atoi(s);
}
This is one of the attributes that does carry semantics. If a [[noreturn]] function actually returns, the behavior is undefined. The optimizer may already have assumed nothing follows the call. GCC catches the obvious cases:
[[noreturn]] void bad(int x) { if (x) std::exit(1); }
// warning: 'noreturn' function does return
It cannot catch every path, especially when the function calls something that returns only in rare conditions. A known failure mode is a logging “fatal” helper that someone later changes to “log and continue” in test builds, while it keeps [[noreturn]]. The code after the call was compiled on the assumption it could never run, so the test build then misbehaves in ways that are very hard to explain from the source. If a function’s “never returns” guarantee depends on configuration, do not mark it.
[[fallthrough]]
In a switch, falling from one case into the next is legal, but it is a classic bug when someone simply forgot a break. With -Wimplicit-fallthrough (enabled by -Wextra in GCC) the compiler warns on every fall-through, and [[fallthrough]]; marks the ones you meant.
int weight(int x) {
int r = 0;
switch (x) {
case 1: r += 1; // warning: this statement may fall through
case 2: r += 2; break;
case 3: r += 3; [[fallthrough]]; // intentional, no warning
case 4: r += 4; break;
}
return r;
}
The attribute is a statement: it needs the trailing semicolon, and it must be the last thing before the next case or default label. If no label follows it, GCC warns instead of silently accepting it:
switch (x) { case 1: x++; [[fallthrough]]; }
// warning: attribute 'fallthrough' not preceding a case label or default label
Case labels with no statements between them (case 'a': case 'b': ...) never need the attribute; they are not treated as fall-through.
[[likely]] and [[unlikely]] (C++20)
These mark a branch or case label as more or less likely to run. They go on a statement: the body after if or else, or a case label.
int classify(int x) {
if (x > 0) [[likely]] { return 1; }
else [[unlikely]] { return 0; }
}
switch (kind) {
case Kind::Common: [[likely]] return fastPath();
default: return slowPath();
}
What they actually change is code layout. The compiler tends to put the likely block on the fall-through path and move the unlikely block out of the hot instruction stream. They do not program the CPU’s branch predictor, which learns from runtime history no matter how the code is laid out. So for a branch that is well-predicted at runtime, these attributes usually make no measurable difference, and for a branch that really is random (50/50), no hint helps.
The trap is a wrong hint. Tagging the common path [[unlikely]] pushes it into cold code and can make it slower. Hints also go stale as workloads change: an error path that was rare at launch can become common after a new client starts sending bad input. Use them where you have profiled and seen the layout matter, and consider profile-guided optimization (-fprofile-generate / -fprofile-use in GCC), which gets real branch frequencies instead of guesses. The companion article on branch prediction shows how to measure whether a branch is costing you anything.
[[no_unique_address]] (C++20)
This one changes layout. Normally every member must have a distinct address, so even an empty member takes at least one byte plus padding. [[no_unique_address]] lets an empty member overlap other members:
struct Empty {};
struct A { int x; Empty e; };
struct B { int x; [[no_unique_address]] Empty e; };
// sizeof(A) == 8, sizeof(B) == 4 with g++ 10.3 on x86-64
This matters for class templates that hold a stateless allocator, comparator or deleter, which previously needed the empty-base-optimization trick. Since it changes sizeof, changing it is an ABI break for types that cross library boundaries. Note also that MSVC ignores the standard spelling for ABI reasons and provides [[msvc::no_unique_address]] instead, so check the output of sizeof on every compiler you ship with.
Combining attributes and vendor namespaces
Attributes can be listed together or in separate brackets; the two forms are equivalent:
[[nodiscard, deprecated("use newApi")]] int oldApi();
[[nodiscard]] [[deprecated("use newApi")]] int oldApi2();
Compiler-specific attributes live in a namespace, such as [[gnu::always_inline]], [[gnu::cold]] or [[clang::...]]. A compiler that does not know the namespace must ignore it, which makes the syntax safe to write but easy to get wrong. A typo is a warning, not an error:
warning: 'foo::bar' scoped attribute directive ignored [-Wattributes]
warning: 'unknownattr' attribute directive ignored [-Wattributes]
A failure mode I have run into: a project builds with -Wno-attributes to silence noise from another compiler’s attributes, and from then on a misspelled [[nodiscrad]] is silently ignored and the protection it was supposed to add never existed. Keep -Wattributes enabled on at least one CI compiler. If you need to write an attribute only some compilers support, check for it with __has_cpp_attribute(name) and hide the result behind a macro, rather than turning off the warning.