OSC++.031: std::any — Type-Erased Values, any_cast, and Runtime Type Inspection

C++ developer working at a dark workstation with abstract code and a branching runtime type-flow diagram.

std::any is a C++17 standard-library type that can own one value of almost any copy-constructible type while hiding that concrete type behind a uniform wrapper. It is useful when the producer and consumer agree that a value may vary at runtime but the exact set of possible types is not practical to encode into a fixed compile-time list.

This lesson follows OSC++.030: std::variant and OSC++.029: std::optional. Those two vocabulary types deliberately communicate more structure to the compiler: optional<T> means one known type may or may not exist, while variant<A, B, C> means exactly one type from a known set is active. std::any moves farther toward runtime flexibility by erasing the stored type from the wrapper’s static interface.

C++ Weekly with Jason Turner introduces std::any and shows the core C++17 usage model.

What You’ll Learn

  • Explain what type erasure means in practical C++.
  • Create empty and populated std::any objects.
  • Check whether an any contains a value.
  • Inspect the stored runtime type.
  • Recover the value safely with std::any_cast.
  • Understand the difference between throwing and pointer forms of any_cast.
  • Replace, reset, emplace, copy, move, and swap stored values.
  • Understand the CopyConstructible requirement.
  • Recognize small-object optimization as an implementation detail rather than a guarantee.
  • Choose between std::any, std::variant, std::optional, templates, and polymorphic interfaces.

What Type Erasure Means

Type erasure means that a concrete type is hidden behind a stable wrapper interface. The wrapper retains enough internal information to manage the value correctly, but normal users of the wrapper do not need the exact concrete type in the wrapper’s own static type. A variable declared as std::any value; can later contain an int, double, std::string, or another eligible type without changing the declaration of value.

The standard-library reference defines std::any in the <any> header and describes it as a type-safe container for single values of any copy-constructible type. The word type-safe is important. The wrapper does not perform arbitrary conversion when you request a value. If the object contains a double, asking for an int is a type mismatch rather than an implicit conversion.

Creating an Empty std::any

A default-constructed std::any is empty. For example, std::any value; creates an object with no contained value. Call value.has_value() to determine whether a value is present. An empty any returns false.

You can later assign a value with syntax such as value = 42;, value = 3.14;, or value = std::string{"ready"};. Each assignment can replace the previous contained object with a value of a different eligible type.

Inspecting the Runtime Type

The member function type() returns a std::type_info reference describing the currently stored type. A check such as value.type() == typeid(std::string) can therefore determine whether a string is stored before attempting to recover it. When the any is empty, type() reports typeid(void).

Runtime type inspection is useful for diagnostics, generic property systems, plugin metadata, scripting bridges, and APIs that intentionally allow callers to attach opaque user data. It should not automatically become the default way to design every interface. If the set of alternatives is known at compile time, std::variant normally communicates that design more clearly and gives the compiler more information.

CppCon 2024 speaker Sebastian Theophil compares several C++ approaches to reusable data structures, including std::variant, virtual interfaces, templates, and std::any.

Recovering a Value with any_cast

std::any_cast is the standard operation used to recover the stored object. If value contains an integer, std::any_cast<int>(value) returns that integer. If it contains a string, std::any_cast<std::string>(value) returns the string value.

The value- and reference-returning forms of any_cast throw std::bad_any_cast when the requested type does not match the stored type. That behavior connects directly to OSC++.015: Exception Handling Basics. If exception-free probing is preferable, the pointer form is usually the better fit.

The Pointer Form Is a Safe Probe

When you call std::any_cast<T>(&value), the result is a pointer to the stored T when the type matches and nullptr when it does not. A compact pattern is: if (auto* p = std::any_cast<int>(&value)), followed by use of *p inside the successful branch.

This form is especially useful when an interface genuinely allows several unrelated runtime types and the caller wants to test for one specific type without using exceptions as ordinary control flow.

Changing or Clearing the Stored Value

Call reset() to destroy the contained object and return the wrapper to the empty state. Call emplace<T>(...) to construct a new value of type T directly inside the any. The helper std::make_any<T>(...) creates an any containing a directly constructed T.

swap() exchanges the states of two std::any objects. Copying an any copies its contained value when one exists. Moving transfers the state into the destination and leaves the source valid but otherwise unspecified, as with many moved-from standard-library objects.

Why Move-Only Types Are a Limitation

The contained type must satisfy the relevant copy-construction requirements. That means std::any is not a direct container for a purely move-only object such as a typical std::unique_ptr<T>. This is one of the most important practical limitations to remember when comparing std::any with custom type-erasure wrappers.

If an interface must hold arbitrary move-only types, the design may need a different wrapper, dynamic polymorphism, a variant over known alternatives, a shared ownership model, or a purpose-built type-erasure abstraction.

CppCon 2023 explores non-virtual polymorphism and several type-erasure techniques, including std::any and std::variant.

Small-Object Optimization

An implementation can store sufficiently small values directly inside the std::any object instead of allocating separate dynamic storage. The standard encourages implementations to avoid dynamic allocation for small objects when appropriate, but the exact buffer size and threshold are implementation details. Code should not depend on a particular compiler or standard library using the same internal size.

This means performance-sensitive software should measure the actual target implementation. A value that fits in one library’s internal buffer may allocate in another. The public semantics remain the same even when the storage strategy differs.

A 2026 r/cpp discussion walks through how type erasure can be derived and includes community discussion of real std::any use cases, implementation strategies, and small-buffer optimization tradeoffs.

std::any vs. std::variant

  • Use std::variant when the set of legal types is known and closed.
  • Use std::any when the wrapper must accept values from an open or externally defined set of copyable types.
  • Variant makes alternatives visible in the type system and enables exhaustive visitation.
  • Any hides alternatives and requires runtime agreement about what type is stored.
  • Variant is usually easier to reason about when all alternatives are under your control.
  • Any is useful for extension points and opaque metadata where the library should carry data without understanding it.

std::any vs. std::optional

std::optional<T> answers one question: is a T present? std::any answers a broader question: is some copyable value present, and what runtime type is it? If the program only needs an integer that may be missing, std::optional<int> is clearer than std::any.

When std::any Is a Good Fit

  • Attaching arbitrary user metadata to nodes or objects owned by a library.
  • Property bags where different keys may carry unrelated runtime types.
  • Plugin systems where extensions need to transport opaque values through framework code.
  • Interoperability layers that must preserve a value whose concrete type is understood only at the boundary.
  • Callback context or user data when ownership and type checking are useful.
  • Diagnostic or scripting systems where the exact type is intentionally discovered at runtime.

When std::any Is Usually the Wrong Tool

  • When all valid alternatives are already known: prefer std::variant.
  • When one known type may be absent: prefer std::optional.
  • When every stored object must expose the same behavior: consider an interface, concept-driven design, or a purpose-built type-erasure wrapper.
  • When compile-time genericity is sufficient: use templates rather than discarding type information.
  • When maximum predictability and minimal runtime overhead are required: benchmark carefully before choosing an open-ended runtime container.

A Small Mental Model

Think of std::any as an owned box with a hidden label. The box can be empty or contain one eligible object. The wrapper knows how to destroy and copy that object. You can ask whether the box has something inside, inspect the hidden type label, and request the value back with the exact expected type. Ask for the wrong type and the safe access mechanism refuses the mismatch.

Hands-On Exercise

  1. Create an empty std::any named data.
  2. Verify that has_value() is initially false.
  3. Assign the integer 42.
  4. Use the pointer form of any_cast to test for int and print the value.
  5. Replace the integer with a std::string.
  6. Inspect the runtime type with type().
  7. Use a throwing any_cast inside a try/catch block and intentionally request the wrong type.
  8. Catch std::bad_any_cast and report the mismatch.
  9. Call reset() and confirm that the object is empty.
  10. Use emplace<std::string> to construct a new string directly.

Knowledge Check

  1. What problem does std::any solve?
  2. Which header defines it?
  3. Which C++ standard introduced it?
  4. What does has_value() report?
  5. What does type() return for an empty any?
  6. What happens when value-returning any_cast requests the wrong type?
  7. What does pointer-form any_cast return on a mismatch?
  8. Why can a typical move-only unique_ptr not be stored directly?
  9. When is std::variant generally clearer?
  10. Is small-object optimization guaranteed to have the same size across implementations?

Answers

  1. It provides a type-safe owning wrapper that can hold one value from an open set of eligible runtime types.
  2. <any>.
  3. C++17.
  4. Whether a contained value is present.
  5. typeid(void).
  6. It throws std::bad_any_cast.
  7. nullptr.
  8. The contained type must satisfy the copy-construction requirements expected by std::any.
  9. When the legal alternatives are known at compile time.
  10. No. The optimization and internal buffer size are implementation details.

References

Primary technical references: cppreference — std::any and cppreference — standard library header <any>. C++17 added std::any, std::optional, and std::variant as standard-library vocabulary types.

Key Takeaway

std::any is runtime flexibility with type-checked recovery. It is strongest when a library needs to transport an owned value without knowing every concrete type in advance. It is weaker when the program already knows its alternatives and would benefit from having those alternatives visible in the static type system.

Rotation next: Semiconductor Technician.

BitcoinVersus.Tech Editor’s Note:
We volunteer daily to ensure the credibility of the information on this platform is Verifiably True. If you would like to support to help further secure the integrity of our research initiatives, please donate using the QR code provided

Bitcoin donation QR code for 5,000 sats.

For best QR-code scanning results, please scan this donation code from a desktop or larger screen. Mobile screens may prevent camera recognition.

Leave a Reply