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++17A 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:
| Problem | Tool |
|---|---|
| Closed set of alternatives, fixed at compile time | std::variant (C++17) |
| Optional value of a single known type | std::optional (C++17) |
| Open set β any type determined at runtime | std::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
#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{}; // equivalentExamples
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:
#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:
#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:
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 neededAccumulating heterogeneous values
std::vector<std::any> is a natural heterogeneous sequence β used in plugin systems, scripting value types, and serialisation intermediaries:
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:
// 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:
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 castCommon 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:
std::any a = std::make_unique<int>(42); // β ill-formed β unique_ptr is not CopyConstructibleUse 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:
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 wrappertype().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:
std::string s = std::any_cast<std::string>(std::move(a));
// a's state is unspecified here
a.reset(); // now a.has_value() == false, guaranteedNot 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 typesTfor whichis_nothrow_move_constructible_v<T>istrue.
The two conditions for inline (non-heap) storage are therefore:
- The object's size and alignment fit within the implementation's internal buffer.
- 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)
| Toolchain | sizeof(std::any) | Inline buffer |
|---|---|---|
| libc++ (Clang) | 32 bytes | ~24 bytes (3 void*) |
| libstdc++ (GCC) | 16 bytes | ~8 bytes (1 void*) |
| MSVC | 64 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
// 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 bytesThe 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.
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
| Scenario | Recommended form | Reason |
|---|---|---|
| Type is known and mismatch is a bug | any_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 copy | any_cast<T&>(a) | Reference into stored value; no copy |
| Transferring ownership out | any_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 chain | Clean, 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.
#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 fallbackExact type match required β no implicit conversions
any_cast uses typeid comparison, not implicit conversion rules. Every cast must name the exact stored type:
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 BasePerformance Profile
Allocation cost
| Operation | SBO path (small type) | Heap path (large type) |
|---|---|---|
| Construction | Copy into inline buffer | operator new + copy |
| Move-construct | Buffer copy (trivially copyable types: memcpy) | Pointer swap β O(1), cheap |
| Copy-construct | Copy into inline buffer | operator new + copy |
| Destruction | In-place destructor call | Destructor + operator delete |
any_cast (pointer form) | typeid comparison + pointer arithmetic | Same |
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
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:
| Criterion | std::any | std::variant<Ts...> | void* | Custom type erasure |
|---|---|---|---|---|
| Type set known at compile time | No | Yes | No | Depends |
| Exhaustiveness checking | No | std::visit | No | Depends |
| Heap allocation for large types | Yes | Never | Never | Tunable |
| Type safety | Yes | Yes | No | Depends |
| Move-only types | No | Yes | Yes | Yes |
| Value semantics (copyable) | Yes | Yes | No | Depends |
| Runtime open type set | Yes | No | Yes | Yes |
| Copy across API boundaries | Yes | Partial | Manual | Depends |
sizeof on 64-bit | 16β64 bytes | max(sizeof(Ts...))+1 | 8 bytes | Tunable |
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 overanywhen the type set is knownstd::optionalβ nullable single-type wrapper with no type-erasure overheadstd::bad_any_castβ exception thrown by the value/referenceany_castoverloads on type mismatch- Type Erasure idiom β hand-rolled type erasure for reference support, performance tuning, or move-only semantics