Skip to content
C++
Library
since C++17
Basic

std::any

Type-safe, type-erased container for a single value of any copy-constructible type, with runtime type queries and checked casts.

std::anysince C++17

A type-safe container that stores a single value of any copy-constructible type and exposes runtime type identity via std::type_info, with checked value extraction through std::any_cast.

Overview

std::any (C++17, <any>) is the type-safe successor to void* for heterogeneous value storage. It carries the stored type's identity alongside the value so every extraction is a checked operation, never a silent misinterpret. The design originated in Boost (boost::any); C++17 standardised the interface.

Three vocabulary types address related problems:

ProblemTool
Closed set of alternatives, fixed at compile timestd::variant (C++17)
Optional value of a single known typestd::optional (C++17)
Open set β€” any type determined at runtimestd::any (C++17)

Storage model and SBO

std::any implementations universally apply small buffer optimisation (SBO): types that fit within an internal buffer (typically 1–3 pointer-sized words β€” exact threshold is implementation-defined and not queryable) are stored inline with no heap allocation. Types exceeding the buffer, or types that are not nothrow_move_constructible, spill to the heap via new. You cannot portably determine which path was taken.

This means std::any is not zero-overhead. A heap-backed any carries an allocation on construction and a deallocation on destruction. Keep this in mind before placing any in tight loops or high-frequency data structures.

Syntax

cpp
#include <any>   // C++17

// Construction
std::any a;                                             // empty
std::any b = 42;                                        // holds int (likely SBO)
std::any c = std::string("hello");                      // holds std::string
std::any d = std::make_any<std::vector<int>>(4, 0);     // C++17: {0,0,0,0}

// In-place construction β€” avoids an intermediate move
a.emplace<std::pair<int,int>>(3, 7);                    // constructs pair in-place

// std::any_cast β€” five overloads
int  n  = std::any_cast<int>(b);            // copy; throws std::bad_any_cast on mismatch
int& r  = std::any_cast<int&>(b);           // lvalue ref; same throw guarantee
int  mv = std::any_cast<int>(std::move(b)); // move out; b left in valid unspecified state
int* p  = std::any_cast<int>(&b);           // pointer; returns nullptr on mismatch (no throw)
const int* cp = std::any_cast<int>(&std::as_const(b)); // const pointer; no throw

// Introspection
b.has_value();        // true
b.type();             // const std::type_info& β†’ typeid(int)
std::any{}.type();    // typeid(void) β€” guaranteed for empty any

// Reset
b.reset();            // b becomes empty
b = std::any{};       // equivalent

Examples

Heterogeneous property bag

A configuration registry where values can be strings, integers, durations, or nested structures without a fixed schema β€” a realistic motivation for std::any:

cpp
#include <any>
#include <string>
#include <unordered_map>
#include <stdexcept>

class PropertyBag {
    std::unordered_map<std::string, std::any> props_;

public:
    template<typename T>
    void set(std::string key, T&& value) {
        props_.insert_or_assign(std::move(key), std::forward<T>(value));
    }

    // Throws std::out_of_range or std::bad_any_cast on failure
    template<typename T>
    T& get(const std::string& key) {
        auto it = props_.find(key);
        if (it == props_.end())
            throw std::out_of_range("key not found: " + key);
        return std::any_cast<T&>(it->second);
    }

    // No-throw conditional access β€” prefer this in non-exceptional paths
    template<typename T>
    T* get_if(const std::string& key) noexcept {
        auto it = props_.find(key);
        return it != props_.end() ? std::any_cast<T>(&it->second) : nullptr;
    }
};

PropertyBag cfg;
cfg.set("timeout_ms",  5'000);
cfg.set("endpoint",    std::string("https://api.example.com"));
cfg.set("retry",       true);

int  timeout = cfg.get<int>("timeout_ms");    // 5000
bool retry   = cfg.get<bool>("retry");        // true

if (auto* ep = cfg.get_if<std::string>("endpoint"))
    // *ep is valid here; no exception path
    connect(*ep);

Type-dispatching without exceptions

Use the pointer overload for expected type checks; reserve the value/reference overloads for genuinely unexpected mismatches:

cpp
#include <any>
#include <string>
#include <iostream>

void describe(const std::any& v) {
    if (!v.has_value()) {
        std::cout << "(empty)\n";
    } else if (const auto* i = std::any_cast<int>(&v)) {
        std::cout << "int: " << *i << '\n';
    } else if (const auto* s = std::any_cast<std::string>(&v)) {
        std::cout << "string: " << *s << '\n';
    } else if (const auto* d = std::any_cast<double>(&v)) {
        std::cout << "double: " << *d << '\n';
    } else {
        // type().name() is implementation-defined β€” use only for diagnostics
        std::cout << "unknown type: " << v.type().name() << '\n';
    }
}

Moving out of std::any

When you own the any and want the value without a copy, use the rvalue overload:

cpp
std::any a = std::string(1'000, 'x');

// Moves the string out β€” no string heap copy
std::string s = std::any_cast<std::string>(std::move(a));
a.reset(); // a.has_value() after a move is unspecified; reset explicitly if needed

Accumulating heterogeneous values

std::vector<std::any> is a natural heterogeneous sequence β€” used in plugin systems, scripting value types, and serialisation intermediaries:

cpp
std::vector<std::any> row;
row.emplace_back(42);
row.emplace_back(3.14);
row.emplace_back(std::string("label"));
row.emplace_back(true);

for (const auto& cell : row)
    describe(cell);

Best Practices

Prefer std::variant for closed type sets. If the universe of types is fixed at compile time, variant provides exhaustiveness via std::visit, no heap allocation, and compile-time type checking. Reach for std::any only when the type set is genuinely open at compile time.

Always use the pointer overload for conditional checks. std::any_cast<T>(&a) is noexcept and returns nullptr on mismatch. The value overload throws std::bad_any_cast. Exceptions should signal truly unexpected failures, not routine type inspection.

Construct in-place to avoid redundant moves. Both std::make_any<T>(args...) and a.emplace<T>(args...) construct the value directly in the any's storage:

cpp
// Preferred β€” constructs vector directly in any's storage
auto a = std::make_any<std::vector<std::string>>(100, "slot");

// Worse β€” constructs vector, then moves it in
std::vector<std::string> tmp(100, "slot");
std::any b = std::move(tmp);

Cache a reference for repeated in-place mutation. Casting repeatedly in a loop re-runs the typeid check each time. A single reference cast amortises the cost:

cpp
std::any counter = 0;
int& n = std::any_cast<int&>(counter); // one checked cast
for (int i = 0; i < 1'000'000; ++i)
    ++n; // direct reference β€” no per-iteration cast

Common Pitfalls

Move-only types cannot be stored

std::any requires the contained type's decayed type to be copy-constructible. Move-only types fail at compile time:

cpp
std::any a = std::make_unique<int>(42); // ❌ ill-formed β€” unique_ptr is not CopyConstructible

Use std::shared_ptr (which is copyable) or a custom type-erased interface that owns the lifetime.

References are not preserved β€” only values

std::decay_t strips references and top-level cv-qualifiers before storing. You cannot store a raw reference in std::any:

cpp
int x = 42;
std::any a = x;           // stores a copy of x β€” x and a are independent
std::any b = std::ref(x); // stores std::reference_wrapper<int>, not int&

// Correct extraction when using reference_wrapper:
x = 99;
int val = std::any_cast<std::reference_wrapper<int>>(b).get(); // 99 β€” via wrapper

type().name() is not portable

The string returned by type().name() is implementation-defined and typically mangled. On GCC, typeid(int).name() returns "i"; on MSVC it returns "int". Never use it for serialisation, protocol negotiation, or user-facing output. Use it only for debugging diagnostics.

has_value() after a move is unspecified

Moving out of an any via std::any_cast<T>(std::move(a)) leaves a in a valid but unspecified state β€” it may still report has_value() == true with a moved-from value. Reset explicitly if you need a guaranteed-empty state:

cpp
std::string s = std::any_cast<std::string>(std::move(a));
// a's state is unspecified here
a.reset(); // now a.has_value() == false, guaranteed

Not suitable for performance-critical paths

Each any_cast performs a typeid comparison (cheap, but not free). Construction of a large type allocates on the heap. If you're processing millions of heterogeneous values per second, profile against std::variant with std::visit β€” the closed-set version typically wins on both branch prediction and allocation cost.

Small Buffer Optimization In Depth

The C++17 standard recommends but does not mandate SBO. The exact wording ([any.class]):

Implementations should avoid the use of dynamically allocated memory for a small contained object. [Example: where the object constructed is holding only an int.] Such small-object optimization shall only be applied to types T for which is_nothrow_move_constructible_v<T> is true.

The two conditions for inline (non-heap) storage are therefore:

  1. The object's size and alignment fit within the implementation's internal buffer.
  2. The type satisfies std::is_nothrow_move_constructible_v<T> β€” this prevents a partially-constructed buffer from being left in an inconsistent state if a move throws.

What fits in the SBO buffer (implementation survey)

Toolchainsizeof(std::any)Inline buffer
libc++ (Clang)32 bytes~24 bytes (3 void*)
libstdc++ (GCC)16 bytes~8 bytes (1 void*)
MSVC64 bytes~56 bytes

These sizes are implementation-defined and subject to change across versions. std::any has no API to query the buffer size or to determine at runtime whether a given value was stored inline or on the heap.

Practical SBO heuristics

cpp
// Almost certainly inline on all major implementations:
std::any a1 = 42;               // int β€” fits everywhere
std::any a2 = 3.14;             // double β€” fits everywhere
std::any a3 = true;             // bool β€” fits everywhere
std::any a4 = (void*)(nullptr); // pointer β€” fits everywhere

// Borderline β€” fits in Clang/MSVC, NOT in GCC's smaller buffer:
std::any a5 = std::pair<int,int>(1, 2); // 8 bytes β€” only fits if buffer >= 8

// Heap-allocated on all major implementations:
std::any a6 = std::string("hello");   // std::string contains a pointer + size + cap
std::any a7 = std::vector<int>{1,2};  // vector is 3 pointers = 24 bytes

The key insight: std::string is itself a type that internally uses SSO, but as a value stored inside std::any it typically exceeds the any buffer on GCC (16-byte string + vtable pointer > 8-byte buffer). On Clang or MSVC the std::string SSO variant may fit inline.

boost::any does not use SBO

The original boost::any predates the SBO requirement and unconditionally allocates on the heap for every stored value, making it 8 bytes (just a pointer). std::any trades a larger sizeof for the elimination of heap allocation on small types.

any_cast Forms and Exception Safety

std::any_cast has five overloads with distinct semantics and error-handling contracts. Choosing the right form is critical for correctness and performance.

cpp
std::any a = std::string("world");

// --- Throwing overloads (1-3) ---

// Overload 1: const lvalue β†’ returns T by value (copy-constructs)
std::string s1 = std::any_cast<std::string>(a);
// Throws std::bad_any_cast if typeid(T) != typeid(stored type)

// Overload 2: lvalue β†’ returns T& or const T& (reference into stored value)
std::string& r = std::any_cast<std::string&>(a);
r += " modified"; // writes back into 'a'

const std::string& cr = std::any_cast<const std::string&>(a);

// Overload 3: rvalue β†’ moves the stored value out
std::string s2 = std::any_cast<std::string>(std::move(a));
// 'a' is left in a valid but unspecified state

// --- Non-throwing overloads (4-5) ---

// Overload 4: pointer to const any β†’ returns const T* or nullptr
const std::string* cp = std::any_cast<std::string>(&std::as_const(a));

// Overload 5: pointer to any β†’ returns T* or nullptr (noexcept)
std::string* p = std::any_cast<std::string>(&a);
if (p) {
    *p += " via pointer"; // safe in-place mutation
}

Choosing between throwing and pointer forms

ScenarioRecommended formReason
Type is known and mismatch is a bugany_cast<T>(a) (value/ref)Exception signals programmer error; no branch needed
Conditional type inspection (common path)any_cast<T>(&a) (pointer)noexcept, no unwinding overhead, works in if init-statements
Mutation without copyany_cast<T&>(a)Reference into stored value; no copy
Transferring ownership outany_cast<T>(std::move(a))Move-constructs from stored value; avoids a copy for large types
Runtime type dispatch (describe() pattern)Pointer form in if-else chainClean, no exception machinery involved

Exception guarantees

std::bad_any_cast derives from std::bad_cast which derives from std::exception. It carries no payload beyond what(). The class provides the strong exception guarantee: if any_cast throws, the any object is unchanged.

cpp
#include <any>
#include <stdexcept>

// Safe extraction with a fallback default
template<typename T>
T any_cast_or(const std::any& a, T fallback) noexcept {
    const T* p = std::any_cast<T>(&a);
    return p ? *p : std::move(fallback);
}

std::any val = 42;
int n = any_cast_or<int>(val, -1);    // 42
double d = any_cast_or<double>(val, 0.0); // 0.0 β€” type mismatch, returns fallback

Exact type match required β€” no implicit conversions

any_cast uses typeid comparison, not implicit conversion rules. Every cast must name the exact stored type:

cpp
std::any a = 42;              // stored as int
// All of these return nullptr / throw, even though the value would fit:
std::any_cast<long>(&a);      // nullptr β€” long != int
std::any_cast<int64_t>(&a);   // nullptr β€” int64_t != int (on most platforms)

// Inheritance does NOT work:
struct Base { };
struct Derived : Base { };
std::any d = Derived{ };
std::any_cast<Base>(&d);      // nullptr β€” stored type is Derived, not Base

Performance Profile

Allocation cost

OperationSBO path (small type)Heap path (large type)
ConstructionCopy into inline bufferoperator new + copy
Move-constructBuffer copy (trivially copyable types: memcpy)Pointer swap β€” O(1), cheap
Copy-constructCopy into inline bufferoperator new + copy
DestructionIn-place destructor callDestructor + operator delete
any_cast (pointer form)typeid comparison + pointer arithmeticSame

Move-constructing a heap-backed any is cheap (pointer swap), but move-constructing an SBO-backed any still copies the inline buffer β€” there is no "steal" because the buffer is part of the object itself.

any_cast overhead

Each any_cast performs one typeid comparison (comparing two std::type_info pointers). On all major implementations this is a single pointer equality check and is essentially free in isolation. In a type-dispatch chain across N candidate types, the worst case is N comparisons β€” O(N) vs std::variant's O(1) discriminant check.

Comparison with std::variant

cpp
std::variant<int, double, std::string> v = 42;

// O(1): variant stores an integer discriminant; std::visit jumps via index
std::visit([](auto& x) { ... }, v);

std::any a = 42;
// O(N) for N candidate types: any_cast chain does N typeid comparisons
if (auto* i = std::any_cast<int>(&a)) { ... }
else if (auto* d = std::any_cast<double>(&a)) { ... }

Decision Table: std::any vs Alternatives

Use this table to select the right tool before reaching for std::any:

Criterionstd::anystd::variant<Ts...>void*Custom type erasure
Type set known at compile timeNoYesNoDepends
Exhaustiveness checkingNostd::visitNoDepends
Heap allocation for large typesYesNeverNeverTunable
Type safetyYesYesNoDepends
Move-only typesNoYesYesYes
Value semantics (copyable)YesYesNoDepends
Runtime open type setYesNoYesYes
Copy across API boundariesYesPartialManualDepends
sizeof on 64-bit16–64 bytesmax(sizeof(Ts...))+18 bytesTunable

When each tool wins

Reach for std::variant when the set of types is fixed at compile time. You get zero heap allocation, O(1) dispatch with std::visit, exhaustiveness enforcement, and storage for move-only types. The cost is recompilation when the type set changes.

Reach for std::any when the type set is genuinely open β€” you are writing a library that does not know the caller's types, implementing a scripting engine value type, passing arbitrary context through a framework that cannot depend on concrete types, or building a property bag where plugins extend the schema.

Reach for void* when you need zero overhead, are interfacing with C APIs, or can guarantee lifetime and type externally (e.g., a C callback void* userdata that only lives one call frame). Accept the full manual safety burden.

Reach for custom type erasure when std::any overhead is measurable in profiling, you need reference semantics, you need to store move-only types, or you want a fixed-size inline buffer whose size is under your control (e.g., inplace_function or a custom any with a tunable SBO threshold).

See Also

  • std::variant β€” discriminated union with compile-time exhaustiveness; prefer over any when the type set is known
  • std::optional β€” nullable single-type wrapper with no type-erasure overhead
  • std::bad_any_cast β€” exception thrown by the value/reference any_cast overloads on type mismatch
  • Type Erasure idiom β€” hand-rolled type erasure for reference support, performance tuning, or move-only semantics