C++ Attributes
Standard attributes provide portable, compiler-understood annotations on declarations, statements, and expressions — [[nodiscard]], [[likely]], [[assume]], and more.
C++ Attributessince C++11Attributes are portable, double-bracket annotations that communicate constraints, hints, or intent to the compiler — replacing a fragmented ecosystem of vendor-specific __attribute__ and __declspec extensions with a single, standardized syntax.
Overview
C++11 introduced [[attribute]] syntax as a unified mechanism for annotating declarations, statements, and expressions. Subsequent standards added new attributes; compiler vendors extend the mechanism with namespaced attributes like [[gnu::always_inline]] and [[clang::no_sanitize(...)]].
Standard attributes fall into three categories:
- Diagnostic attributes — instruct the compiler to emit warnings (
[[nodiscard]],[[deprecated]],[[fallthrough]],[[maybe_unused]]) - Optimizer hints — guide code generation without changing semantics (
[[likely]],[[unlikely]],[[assume]],[[no_unique_address]]) - Control-flow contracts — enforce invariants about execution (
[[noreturn]],[[carries_dependency]])
Since C++17, unknown attributes in a vendor namespace are required to be silently ignored by conforming implementations. This means [[gnu::noinline]] on a Clang build is safe without #ifdef guards — though the converse (GCC ignoring [[clang::...]]) depends on GCC version.
Syntax
Attributes can be placed in multiple syntactic positions:
// On declarations
[[nodiscard]] int open(const char* path); // C++17
[[deprecated("use open_v2")]] int open_legacy(const char*); // C++14
[[maybe_unused]] static void debug_dump(); // C++17
// On a type — propagates to every function returning it
struct [[nodiscard]] Error { int code; }; // C++17
// On statements
switch (cmd) {
case Command::HardReset:
reset();
[[fallthrough]]; // C++17 — must be a statement, not a comment
case Command::Init:
init();
break;
}
// On branches and labels (C++20)
if ([[likely]] x > 0) { fast_path(x); }
else [[unlikely]] { slow_path(x); }
switch (state) {
[[likely]] case State::Running: tick(); break; // C++20
[[unlikely]] case State::Error: handle(); break; // C++20
}
// Optimizer assumption — statement form (C++23)
void f(int* p, int n) {
[[assume(n > 0)]]; // UB if false at runtime
[[assume(p != nullptr)]];
}Examples
[[nodiscard]] — C++17; message form C++20
Warn when a return value is silently discarded. Essential for error codes, resource handles, and factory functions.
// C++17: basic form
[[nodiscard]] int connect(std::string_view host, uint16_t port);
// C++20: message shown verbatim in the diagnostic
[[nodiscard("ignoring this leaks the socket fd")]]
FileDescriptor open_socket(std::string_view path);
// C++17: annotate the type, not just the function — stronger form
// The annotation follows the type through typedefs and aliases.
struct [[nodiscard]] Status {
int code;
bool ok() const { return code == 0; }
explicit operator bool() const { return ok(); }
};
Status write_all(int fd, std::span<const std::byte> buf); // C++20 span
void sink(int fd, std::span<const std::byte> buf) {
write_all(fd, buf); // warning: ignoring [[nodiscard]] Status
(void)write_all(fd, buf); // explicit discard — suppresses warning
if (!write_all(fd, buf)) handle_error(); // correct usage
}Annotating the type rather than the function is preferable: every call site is covered automatically, including return values stored through generic wrappers.
[[deprecated]] — C++14
// C++14
[[deprecated("use connect_v2(); argument order changed to (host, port)")]]
bool connect(int port, const char* host);
// On an enum value — C++17
enum class Codec { H264, [[deprecated("use Codec::AV1")]] VP9, AV1 };
// On a namespace — C++17
namespace [[deprecated("types moved to ::mylib")]] mylib_v1 { }Write deprecation messages that name the replacement and explain the breaking change. The string is shown verbatim in the diagnostic; treat it as documentation.
[[fallthrough]] — C++17
void dispatch(Command cmd, State& s) {
switch (cmd) {
case Command::HardReset:
s.clear_pending_io();
[[fallthrough]]; // intentional: hard reset also executes soft reset
case Command::SoftReset:
s.reset_registers();
s.load_defaults();
break;
case Command::Nop:
break;
}
}[[fallthrough]] must appear as a statement immediately before the next case or default label. A // fall through comment does not suppress the warning on any major compiler.
[[maybe_unused]] — C++17
// Parameter unused in one build configuration
void on_resize(int w, int h, [[maybe_unused]] int display_id) {
layout_.resize(w, h); // display_id only meaningful in multi-monitor builds
}
// RAII guard — the destructor is the point, not the variable
[[maybe_unused]] auto lock = std::scoped_lock{mtx_};
// Assertion-only value — unused in NDEBUG builds
[[maybe_unused]] bool ok = cache_.insert(key, val);
assert(ok);[[likely]] / [[unlikely]] — C++20
void process(std::span<const int> data) {
for (int x : data) {
if ([[unlikely]] x < 0) { // errors are rare — cold path
log_negative(x);
continue;
}
accumulate(x); // hot path — optimizer keeps this first
}
}The optimizer uses these hints to lay out instructions (hot code first, fewer branch-not-taken penalties) and for inlining decisions. Apply only after profiling: incorrect hints actively harm performance because the branch predictor adapts at runtime while static layout does not.
[[no_unique_address]] — C++20
Permits a non-static data member to overlap with other members if it is an empty type (no non-static data, no virtual functions). Critical for zero-overhead policy and allocator storage.
template<typename T, typename Hash = std::hash<T>, typename Eq = std::equal_to<T>>
class FlatSet {
[[no_unique_address]] Hash hash_{}; // 0 bytes when stateless
[[no_unique_address]] Eq eq_{}; // 0 bytes when stateless
std::vector<T> data_{};
};
static_assert(sizeof(FlatSet<int>) == sizeof(std::vector<int>));MSVC caveat: For stable binary layout across translation units compiled with different compiler versions, MSVC requires [[msvc::no_unique_address]] rather than [[no_unique_address]]. The standard attribute compiles but uses a legacy layout. Guard with a compatibility macro when targeting MSVC.
Two [[no_unique_address]] members of the same type cannot share an address — the standard requires that distinct objects of the same type have distinct addresses. Use distinct tag types to work around this.
[[assume(expr)]] — C++23
Communicates an invariant the optimizer can exploit. If the expression is false at runtime, the behavior is undefined — no diagnostic, no trap, no defined fallback.
void scale_avx(float* __restrict__ data, int n, float factor) {
[[assume(n > 0)]];
[[assume(n % 8 == 0)]]; // C++23 — full SIMD lanes
[[assume(reinterpret_cast<uintptr_t>(data) % 32 == 0)]]; // AVX alignment
for (int i = 0; i < n; ++i)
data[i] *= factor;
// The compiler can now emit a pure vectorized loop with no scalar
// remainder or alignment-fixup preamble.
}Pre-C++23 equivalents: __builtin_assume(expr) (Clang), __assume(expr) (MSVC), and if (!(expr)) __builtin_unreachable(); (GCC). C++23 unifies these. For safety, pair with an assertion in debug builds:
void f(int n) {
assert(n > 0); // checked in debug builds
[[assume(n > 0)]]; // optimizer hint in release (C++23)
}[[noreturn]] — C++11
[[noreturn]] void fatal(std::string_view msg) {
std::fprintf(stderr, "FATAL: %.*s\n", (int)msg.size(), msg.data());
std::terminate();
}
// Eliminates spurious "control reaches end of non-void function" warnings
int safe_divide(int a, int b) {
if (b == 0) fatal("division by zero"); // compiler knows execution stops here
return a / b;
}The standard library marks std::terminate, std::abort, std::exit, std::quick_exit, and std::longjmp as [[noreturn]]. If a [[noreturn]] function actually returns, the behavior is undefined.
[[carries_dependency]] — C++11
Used with std::memory_order_consume to propagate dependency chains for lock-free code on weakly-ordered architectures (ARM, POWER). In practice, all major compilers promote consume to acquire, making this attribute a no-op. Avoid unless you are specifically targeting a consume-based optimization on a non-x86 architecture and have verified compiler support.
Best Practices
- Apply
[[nodiscard]]to every function that returns an error code, a resource handle, or a factory result — it costs nothing and catches silent bugs at compile time. - Annotate the type rather than individual functions when the return type always carries significance.
- Write deprecation messages as migration guides, not just notices.
- Never add
[[likely]]/[[unlikely]]without profiling data; the branch predictor is already effective, and wrong hints produce measurable regressions. - Treat
[[assume]]as a contract: document the invariant, enforce it withassertin debug builds, add the assumption for release.
Common Pitfalls
(void)f() suppresses [[nodiscard]] — this is intentional for callers that genuinely don't need the result, but easy to abuse. Prefer documenting why the result is discarded.
[[fallthrough]] position is syntactic — it must be a statement before a case label, not a comment and not after break. Misplacement produces its own warning on some compilers.
[[assume]] is not a debug assertion — it performs no runtime check. An untrue assumption silently corrupts generated code. Always pair with assert guarded by #ifdef NDEBUG.
[[no_unique_address]] and same-type members — two members of the same empty type cannot overlap. Create distinct empty tag types if you need multiple of them.
Vendor attributes pre-C++17 — unknown attributes were not guaranteed to be ignored before C++17. Use __has_cpp_attribute(gnu::noinline) to guard vendor attributes in headers that must compile under older standards.
Compiler Extension Quick Reference
// GCC / Clang — namespaced form (C++11+)
[[gnu::always_inline]] inline void f();
[[gnu::noinline]] void g();
[[gnu::pure]] int h(int); // reads globals, no side effects
[[gnu::const]] int k(int); // no globals, no side effects (stronger than pure)
[[gnu::cold]] void rarely(); // optimize for size; placed in cold section
[[gnu::hot]] void often(); // optimize for speed
// Clang-specific
[[clang::no_sanitize("address", "undefined")]] void f();
[[clang::noinline]] void g();
// MSVC
[[msvc::noinline]] void f(); // VS 2019+
[[msvc::no_unique_address]] Empty e; // ABI-stable form of [[no_unique_address]]Use __has_cpp_attribute(nodiscard) (returns the standard year as an integer, e.g. 201907L for C++20) to guard attribute usage in portable headers.
Attribute Summary Table
| Attribute | Since | Applies To | Purpose |
|---|---|---|---|
[[noreturn]] | C++11 | functions | Function never returns normally |
[[carries_dependency]] | C++11 | functions, parameters | Consume memory-order dependency chain |
[[deprecated]] | C++14 | most declarations | Warn on use |
[[deprecated("msg")]] | C++14 | most declarations | Same, with actionable message |
[[fallthrough]] | C++17 | statements | Suppress implicit-fallthrough warning |
[[nodiscard]] | C++17 | functions, types | Warn if return value discarded |
[[maybe_unused]] | C++17 | most declarations | Suppress unused warning |
[[nodiscard("msg")]] | C++20 | functions | Same, with custom message |
[[likely]] | C++20 | statements, labels | Branch is probably taken |
[[unlikely]] | C++20 | statements, labels | Branch is rarely taken |
[[no_unique_address]] | C++20 | data members | Empty member takes no space |
[[assume(expr)]] | C++23 | statements | Optimizer invariant (UB if false) |
See Also
- noexcept specifier — related control-flow contract for exception guarantees
- constexpr — compile-time evaluation specifier often paired with
[[nodiscard]] - std::unreachable — C++23 complement to
[[assume]]and[[noreturn]] - RAII —
[[nodiscard]]and[[no_unique_address]]are common in RAII guard types