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) |
[[nodiscard]] with reason (C++20)
The message form turns a silent warning into actionable documentation. The string literal is embedded verbatim in the diagnostic, so it should name the consequence of discarding — not just the function.
// C++20 — message shown in the compiler diagnostic
[[nodiscard("ignoring the return value leaks the file descriptor")]]
int open_file(const char* path, int flags);
[[nodiscard("error is silently lost; check .ok() before proceeding")]]
struct WriteResult { bool ok() const; int bytes_written; };
WriteResult write_record(int fd, std::span<const std::byte> data); // C++20
// Wrong — the compiler will warn with the custom message:
// warning: ignoring return value of 'open_file', declared with
// attribute 'nodiscard': "ignoring the return value leaks the file descriptor"
void bad_caller() {
open_file("/tmp/x", O_RDONLY); // C++20 warning includes the reason string
}The reason string is especially valuable in APIs where the return type alone (int, bool) does not make the consequence obvious. Without a message, developers may cargo-cult a (void) cast to silence the warning without understanding why the result matters.
[[likely]] and [[unlikely]] (C++20)
These attributes guide the compiler's static branch layout, not the CPU's dynamic branch predictor. The CPU learns branch behavior at runtime; static hints affect instruction scheduling and code placement in the object file, which matters most when the branch predictor has not warmed up (startup paths, rare error handling, JIT-compiled code).
// C++20 — annotate if/else branches
int parse_token(const char* src) {
if ([[likely]] *src >= '0' && *src <= '9') {
return *src - '0'; // hot path — placed first in object code
} else [[unlikely]] {
return -1; // cold path — optimizer may outline it
}
}
// C++20 — annotate switch/case labels
void dispatch_event(Event e) {
switch (e.type) {
[[likely]] case EventType::MouseMove: handle_mouse(e); break;
[[likely]] case EventType::KeyDown: handle_key(e); break;
[[unlikely]] case EventType::Resize: handle_resize(e); break;
[[unlikely]] case EventType::Quit: handle_quit(e); break;
}
}
// Incorrect placement — [[likely]] annotates the statement, not a label
for (auto& item : items) {
if ([[unlikely]] item.is_sentinel()) break; // rare early exit
process(item);
}A common mistake is applying [[likely]] to the if keyword when the hot path is in the else branch — double-check which sub-statement the attribute annotates. Profile before adding these; they are most effective on tight inner loops.
[[assume]] (C++23)
[[assume(expr)]] is an optimizer contract, not a runtime guard. If the expression evaluates to false at any point, the behavior is undefined — the compiler is entitled to delete surrounding code, hoist loads, or otherwise transform the program under the assumption the condition always holds.
// C++23 — alignment assumption enables auto-vectorization without preamble
void dot_product(const float* __restrict__ a,
const float* __restrict__ b,
float* __restrict__ out, int n) {
[[assume(n > 0)]];
[[assume(n % 16 == 0)]]; // C++23
[[assume(reinterpret_cast<uintptr_t>(a) % 64 == 0)]]; // cache-line aligned
[[assume(reinterpret_cast<uintptr_t>(b) % 64 == 0)]];
for (int i = 0; i < n; ++i)
out[i] = a[i] * b[i];
}
// Safe pattern: assert in debug, assume in release
#ifndef NDEBUG
# define PRECONDITION(expr) assert(expr)
#else
# define PRECONDITION(expr) [[assume(expr)]]
#endif
void process(int* data, int size) {
PRECONDITION(size > 0); // checked in debug, assumed in release
PRECONDITION(data != nullptr);
}Before C++23, the same effect required __builtin_assume(expr) (Clang/GCC) or __assume(expr) (MSVC). C++23 standardizes the spelling; the compiler-specific builtins remain available for older standard modes.
Vendor attributes: GCC and Clang
Vendor attributes follow the [[vendor::name]] syntax introduced in C++11 and must be silently ignored by other compilers since C++17. This makes them safe in shared headers without #ifdef guards when the minimum standard is C++17.
// GCC and Clang — both recognize [[gnu::...]] attributes
[[gnu::always_inline]] inline int fast_abs(int x) { return x < 0 ? -x : x; }
[[gnu::noinline]] void debug_dump(const State&);
[[gnu::cold]] void out_of_memory_handler(); // placed in cold section
[[gnu::hot]] void inner_loop(float*, int); // optimized aggressively
[[gnu::pure]] int checksum(const void*, size_t); // no side effects, reads mem
[[gnu::const]] float fast_rsqrt(float); // no side effects, no mem reads
// Clang-specific — not recognized by GCC
[[clang::no_sanitize("address")]] void bypass_asan(char* p);
[[clang::no_sanitize("undefined")]] int unchecked_shift(int x, int n);
[[clang::no_sanitize("address","undefined")]] void raw_buffer_op(void*, size_t);
[[clang::always_inline]] inline void clang_force_inline();
// MSVC-specific
[[msvc::noinline]] void msvc_no_inline(); // VS 2019 16.6+
[[msvc::no_unique_address]] Empty compressed_member; // ABI-stable empty-base opt
// Guarding with __has_cpp_attribute — returns 0 if unknown
#if __has_cpp_attribute(gnu::always_inline)
# define FORCE_INLINE [[gnu::always_inline]] inline
#elif __has_cpp_attribute(msvc::forceinline)
# define FORCE_INLINE [[msvc::forceinline]] inline
#else
# define FORCE_INLINE inline
#endifNote that [[gnu::pure]] and [[gnu::const]] differ: pure may read global memory while const must not — violating the stronger contract is UB that the optimizer will exploit silently.
Attribute propagation in templates
Standard attributes on a template declaration propagate to each instantiation, but vendor attributes applied inside a template body interact differently with inlining decisions. Understanding which attributes attach to the template vs. the instantiation is critical for correctness.
// [[nodiscard]] propagates to all specializations — C++17
template<typename T>
[[nodiscard]] T parse(std::string_view sv);
// [[deprecated]] on a primary template marks all instantiations
template<typename T>
[[deprecated("use parse<T> instead")]] T old_parse(std::string_view sv);
// Explicit specialization can re-apply or remove the attribute
template<>
[[nodiscard]] int parse<int>(std::string_view sv); // C++17 — redundant but legal
// [[maybe_unused]] prevents warnings on unused type parameters
template<typename [[maybe_unused]] Tag, typename T> // C++17
struct StrongTypedef { T value; };
// [[no_unique_address]] in CRTP base storage — C++20
template<typename Derived, typename Allocator = std::allocator<int>>
class Base {
[[no_unique_address]] Allocator alloc_{}; // zero bytes when stateless
};A [[nodiscard]] on a class template marks every instantiation; if one specialization intentionally allows discarding, you must add an explicit (void) cast at each such call site — there is no way to "un-nodiscard" a specialization.
Writing portable attribute wrappers
Wrapping attributes in macros or feature-test macros lets a single codebase target C++14 through C++23 without scattering #ifdef blocks. The key tool is __has_cpp_attribute, which returns the attribute's feature-test value or 0 if unavailable.
// Portable wrappers — place in a project-wide "compat.hpp"
// [[nodiscard]] with optional reason (reason requires C++20)
#if __has_cpp_attribute(nodiscard) >= 201907L // C++20 value
# define NODISCARD(msg) [[nodiscard(msg)]]
#elif __has_cpp_attribute(nodiscard) // C++17
# define NODISCARD(msg) [[nodiscard]]
#else
# define NODISCARD(msg)
#endif
// [[likely]] / [[unlikely]] — C++20
#if __has_cpp_attribute(likely)
# define LIKELY [[likely]]
# define UNLIKELY [[unlikely]]
#else
# define LIKELY
# define UNLIKELY
#endif
// [[assume]] — C++23; fall back to compiler builtins
#if __has_cpp_attribute(assume) // C++23
# define ASSUME(expr) [[assume(expr)]]
#elif defined(__clang__)
# define ASSUME(expr) __builtin_assume(expr)
#elif defined(_MSC_VER)
# define ASSUME(expr) __assume(expr)
#elif defined(__GNUC__)
# define ASSUME(expr) do { if (!(expr)) __builtin_unreachable(); } while (0)
#else
# define ASSUME(expr) ((void)0)
#endif
// Usage
NODISCARD("return value must be checked")
int open_resource(const char* path);
void tight_loop(float* data, int n) {
ASSUME(n > 0);
ASSUME(n % 8 == 0);
for (int i = 0; i < n; ++i) {
if (UNLIKELY(data[i] < 0.0f)) data[i] = 0.0f;
}
}Feature-test values for reference: [[nodiscard]] → 201603L (C++17), 201907L (C++20 with reason); [[likely]]/[[unlikely]] → 201803L (C++20); [[assume]] → 202207L (C++23).
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