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