Some problems genuinely require holding a value whose type isn’t known until run time — plug-in systems where a host loads arbitrary user data, generic event-handler context attached to messages, configuration values from a file whose schema isn’t known at compile time. The C-era answer was void* plus manual bookkeeping, with all the lifetime and type-safety problems that implies.
C++17’s std::any is the type-safe replacement: a single value type that can hold any copy-constructible object, remembers what it actually contains, and refuses to hand back the wrong type. The catch is that “I might need to hold any type” is a much rarer requirement than people assume — most uses of std::any would be better served by std::variant.
Three things to take away:
std::anyholds a value of any copy-constructible type and usestypeidto verify the type at access time, throwingstd::bad_any_cast(or returningnullptr, depending on the cast form) on a mismatch.- Two access shapes:
std::any_cast<T>(a)throws on mismatch,std::any_cast<T>(&a)returns a pointer that’s null on mismatch — prefer the pointer form for control flow. - Reach for
std::anyonly when the type set is genuinely open; for a closed set known at compile time,std::variantis almost always the right tool.
What std::any actually is
std::any is a value type that can store any object that can be copy-constructed. Internally, it carries the contained object’s type identity (a const std::type_info&, the same kind that typeid produces) and the storage for the object itself.
#include <any>
#include <iostream>
#include <string>
int main(){
std::any a; // empty
a = 42; // now holds int
a = std::string{"hello"}; // int destroyed, now holds string
a = 3.14; // string destroyed, now holds double
std::cout << a.has_value() << '\n'; // 1
std::cout << a.type().name() << '\n'; // implementation-defined
// (typically "d" for double)
}
Assigning into an any runs the destructor of whatever it previously held and copy- or move-constructs the new value in its place. Querying has_value() and type() is cheap, but they don’t give you the value back — for that you need a cast.
Two ways to get the value out
Reading a value from std::any requires naming the type explicitly. There are two cast shapes, and they have very different failure semantics:
std::any a = 42;
// Throwing form: std::any_cast<T>(any)
int x = std::any_cast<int>(a); // OK, returns 42
// std::string s = std::any_cast<std::string>(a); // throws std::bad_any_cast
// Pointer form: std::any_cast<T>(&any)
if (auto* p = std::any_cast<int>(&a)) { // returns int*, or nullptr
std::cout << "Got int: " << *p << '\n';
}
The throwing form is appropriate when a wrong type indicates a genuine bug — you’ve already validated the type elsewhere and just want the value out. The pointer form is appropriate when the wrong type is part of normal control flow, the way you’d test for the held alternative of a std::variant (Nibble #058).
A useful idiom combines type() with the pointer form for a multi-way dispatch:
void print(const std::any& a){
if (auto* i = std::any_cast<int>(&a)) std::cout << "int: " << *i;
else if (auto* s = std::any_cast<std::string>(&a)) std::cout << "string: " << *s;
else if (auto* d = std::any_cast<double>(&a)) std::cout << "double: " << *d;
else std::cout << "unknown type";
std::cout << '\n';
}
Note the asymmetry with std::variant: std::visit over a variant gives you a compile-time exhaustiveness check, because the alternatives are listed in the type. With any, the type set is open, so the compiler cannot warn you about the fallthrough case. That openness is exactly what any provides — and exactly what makes it riskier to use.
A heterogeneous container
The title’s use case is std::vector<std::any> — a container that mixes different types in a single sequence:
std::vector<std::any> userData;
userData.push_back(42);
userData.push_back(std::string{"hello"});
userData.push_back(3.14);
userData.push_back(MyConfigStruct{...});
for (const auto& item : userData) {
print(item); // dispatches by item.type()
}
This works, and it’s type-safe in the sense that the wrong extraction is a defined exception rather than memory corruption. But two costs are worth understanding:
- Per-element overhead. Each
std::anycarries the type identity plus the storage. Most implementations do small-object optimisation — values of up to two or three pointers (typically 16–24 bytes) are stored inline; anything larger is heap-allocated. A vector ofstd::anycontaining strings is therefore a vector ofanyheaders plus per-element heap allocations. - No exhaustiveness check. The dispatch in
printabove is the only place that knows what types might be in the container. Add a new type to apush_backsomewhere, forget to add it toprint, and the program silently falls into the “unknown type” branch with no warning at compile time.
When std::variant is better
For a closed set of types known when you write the container, std::variant<int, std::string, double, MyConfigStruct> is the correct answer almost every time. The advantages are concrete:
- The type set is part of the type — readers of the code know exactly what can be in the container.
std::visitenforces exhaustiveness at compile time.- No heap allocation for any alternative; the variant is sized to fit its largest one.
- Faster access — no
typeidcomparison, just an index check.
The dividing line is honest: if you can list the types now, use variant. If you cannot — because the types come from plugins, scripting layers, generic libraries that don’t know their callers’ types, or genuinely runtime-determined configuration — any is what you reach for.
When std::any is the right tool
A handful of cases do call for std::any:
- Library-style “user data” parameters. A networking library that lets clients attach arbitrary context to a request and have it returned with the response should use
any— the library author cannot enumerate the user’s context types. - Caches keyed by tag, holding heterogeneous values. A type-tagged cache where each entry’s type is determined by its tag rather than its position.
anyplus a tag-to-type map is the safe replacement for avoid*cache. - Configuration trees with open schemas. A TOML/JSON-style document tree where the value at a node could be any of several types and new types may be added later.
- Bridging to scripting languages. Where C++ holds values that originated in (or will be passed back to) a dynamically-typed environment.
In all of these, the type set is genuinely open at compile time. That is the bar any is designed to clear.
Takeaway
std::any is the type-safe successor to void* for problems that genuinely require runtime type heterogeneity. Use the pointer form of std::any_cast when a type mismatch is part of normal control flow; use the throwing form only when a mismatch is a bug. Be honest about whether the type set is genuinely open — std::variant (Nibble #058) is the right answer for any closed set, and brings compile-time exhaustiveness checks and zero heap allocation that any cannot. The rule is simple: reach for any when you really cannot list the types in advance, and only then.