Skip to content
C++
Language
since C++11
Basic

C++ Attributes

Standard attributes provide portable, compiler-understood annotations on declarations, statements, and expressions — [[nodiscard]], [[likely]], [[assume]], and more.

C++ Attributessince C++11

Attributes 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:

cpp
// 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.

cpp
// 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

cpp
// 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

cpp
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

cpp
// 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

cpp
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.

cpp
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.

cpp
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:

cpp
void f(int n) {
    assert(n > 0);      // checked in debug builds
    [[assume(n > 0)]];  // optimizer hint in release (C++23)
}

[[noreturn]] — C++11

cpp
[[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 with assert in 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

cpp
// 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

AttributeSinceApplies ToPurpose
[[noreturn]]C++11functionsFunction never returns normally
[[carries_dependency]]C++11functions, parametersConsume memory-order dependency chain
[[deprecated]]C++14most declarationsWarn on use
[[deprecated("msg")]]C++14most declarationsSame, with actionable message
[[fallthrough]]C++17statementsSuppress implicit-fallthrough warning
[[nodiscard]]C++17functions, typesWarn if return value discarded
[[maybe_unused]]C++17most declarationsSuppress unused warning
[[nodiscard("msg")]]C++20functionsSame, with custom message
[[likely]]C++20statements, labelsBranch is probably taken
[[unlikely]]C++20statements, labelsBranch is rarely taken
[[no_unique_address]]C++20data membersEmpty member takes no space
[[assume(expr)]]C++23statementsOptimizer 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