Understanding std::any in Modern C++

Overview

C++17 introduced three utility types commonly referred to as the "trio": std::optional, std::any, and std::variant. This article focuses on std::any, a type-safe container designed to hold a single value of any copy-constructible type.

The class is defined in the <any> header:

namespace std {
class any;
}

Unlike template classes, std::any is a concrete class that acts as a type-erasing wrapper. It can store fundamental types (int, double, char) as well as compound types (classes, structs). The key advantage is that std::any maintains type information at runtime, enabling type-safe retrieval while allowing arbitrary type storage during declaration.

Construction Methods

Default and Direct Initialization

An empty std::any can be created using the default constructor:

std::any container;

Values can be assigned directly upon construction:

std::any val1 = 100;           // stores int
std::any val2 = "sample text"; // stores const char*

When storing types different from the initializer, use std::in_place_type:

std::any val3{std::in_place_type<double>, 3.14159};
std::any val4{std::in_place_type<std::string>, "initialized"};

Note that array types undergo decay. The following stores a const char*:

std::any val5{std::in_place_type<const char[6]>, "12345"};

For types requiring multiple constructor arguments, either construct the object first or use in_place_type:

std::any v1{std::complex{1.0, 2.0}};
std::any v2{std::in_place_type<std::complex<double>>, 1.0, 2.0};

Initializer lists are also supported:

auto cmp = [](int x, int y) { return std::abs(x) < std::abs(y); };

std::any v3{std::in_place_type<std::set<int, decltype(cmp)>>, 
            {5, -3, 9, 1, -7}, cmp};

std::make_any Helper Function

Two overloads are available:

// Overload 1: Direct construction with arguments
template<class T, class... Args>
std::any make_any(Args&&... args);

// Overload 2: Construction with initializer list
template<class T, class U, class... Args>
std::any make_any(std::initializer_list<U> il, Args&&... args);

The template argument must be explicitly specified:

#include <any>
#include <complex>
#include <functional>
#include <iostream>
#include <string>

int main() {
    auto data1 = std::make_any<std::string>("Greetings!");
    auto data2 = std::make_any<std::complex<double>>(1.5, 3.7);

    std::cout << std::any_cast<std::string&>(data1);
    std::cout << std::any_cast<std::complex<double>&>(data2) << '\n';

    using Callback = std::function<void()>;

    // Attempt 1: Direct assignment (fails)
    std::any data3 = []{ std::cout << "Anonymous function\n"; };
    std::cout << "data3.type() = \"" << data3.type().name() << "\"\n";

    try {
        std::any_cast<Callback>(data3)();
    }
    catch (std::bad_any_cast const&) {
        std::cout << "Type mismatch detected\n";
    }

    // Attempt 2: Using make_any (succeeds)
    auto data4 = std::make_any<Callback>([]{ std::cout << "Callback executed\n"; });
    std::cout << "data4.type() = \"" << data4.type().name() << "\"\n";
    std::any_cast<Callback>(data4)();
}

Output:

Greetings!
(1.5,3.7)
data3.type() = "Z4mainEUlvE_"
Type mismatch detected
data4.type() = "St8functionIFvvEE"
Callback executed

Assignment Operators

Three assignment operators are available:

// Copy assignment
any& operator=(const any& other);

// Move assignment
any& operator=(any&& other) noexcept;

// Value assignment with type deduction
template<typename ValueType>
any& operator=(ValueType&& rhs);

The third overload participates in overload resolution only when std::decay_t<ValueType> differs from any and is copy-constructible:

std::any x = 999;
std::any y = "original";
x = y;                                      // copy assignment
y = 2.71828;                                 // value assignment
x = std::make_any<std::complex>(5.0, 1.0);  // move assignment

Retrieving Values with std::any_cast

Five overloads exist:

// Reference-based casts (throw on mismatch)
template<class T> T any_cast(const any& operand);
template<class T> T any_cast(any& operand);
template<class T> T any_cast(any&& operand);

// Pointer-based casts (return nullptr on mismatch)
template<class T> const T* any_cast(const any* operand) noexcept;
template<class T> T* any_cast(any* operand) noexcept;

Extracting stored values requires casting to the correct type:

std::any_cast<std::string>(val)           // copy
std::any_cast<std::string&>(val)          // mutable reference
std::any_cast<const std::string&>(val)    // const reference

Casting to the wrong type throws std::bad_any_cast:

try {
    auto str = std::any_cast<std::string>(val);
}
catch (std::bad_any_cast& ex) {
    std::cerr << "Caught: " << ex.what() << '\n';
}

To avoid unnecessary temporary object creation, prefer reference or pointer casts:

// Avoid: creates temporary string
std::cout << std::any_cast<std::string>(val);

// Better: const reference
std::cout << std::any_cast<const std::string&>(val);

// Modify value directly
std::any_cast<std::string&>(val) = "new content";

Pointer-based casts allow safe checking without exceptions:

if (auto ptr = std::any_cast<std::string>(&val)) {
    std::cout << "Found: " << *ptr << '\n';
}

Array and function types undergo decay during construction, as std::any uses std::decay_t:

char buffer[12] = "benchmark";
std::any wrapped(buffer);
char* extracted = std::any_cast<char*>(wrapped); // "benchmark"

A comprehensive example:

#include <any>
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>

int main() {
    // Basic usage
    std::any sample1 = 256;
    std::cout << "1) sample1 contains int: " 
              << std::any_cast<int>(sample1) << '\n';

    try {
        auto txt = std::any_cast<std::string>(sample1); // throws
    }
    catch (const std::bad_any_cast& e) {
        std::cout << "2) " << e.what() << '\n';
    }

    // Safe pointer-based checking
    if (int* i = std::any_cast<int>(&sample1))
        std::cout << "3) sample1 is int: " << *i << '\n';
    else if (std::string* s = std::any_cast<std::string>(&sample1))
        std::cout << "3) sample1 is string: " << *s << '\n';
    else
        std::cout << "3) sample1 holds unknown type\n";

    // Reference modification
    sample1 = std::string("modified");
    auto& ref = std::any_cast<std::string&>(sample1);
    ref[0] = 'M';

    std::cout << "4) sample1: " 
              << std::any_cast<std::string const&>(sample1) << '\n';

    // Rvalue reference for move semantics
    auto str = std::any_cast<std::string&&>(std::move(sample1));
    static_assert(std::is_same_v<decltype(str), std::string>);

    // Original any is in valid but unspecified state
    std::cout << "5) original size: " 
              << std::any_cast<std::string>(&sample1)->size() << '\n'
              << "6) moved value: " << str << '\n';
}

Output:

1) sample1 contains int: 256
2) bad any_cast
3) sample1 is int: 256
4) sample1: Modified
5) original size: 0
6) moved value: Modified

Modifier Functions

emplace

Destroys current content and constructs a new object in place:

#include <algorithm>
#include <any>
#include <iostream>
#include <string>
#include <vector>

struct CelestialBody {
    std::string designation;
    int catalogNumber;

    CelestialBody(std::string name, int id) 
        : designation(std::move(name)), catalogNumber(id) {
        std::cout << "CelestialBody::CelestialBody(string, int)\n";
    }

    void describe() const {
        std::cout << "CelestialBody{ \"" << designation 
                  << "\" : " << catalogNumber << " }\n";
    }
};

int main() {
    std::any celestial;
    celestial.emplace<CelestialBody>("Sirius", 2845);
    const auto* body = std::any_cast<CelestialBody>(&celestial);
    body->describe();

    std::any storage;
    storage.emplace<std::vector<char>>({ 'C', '+', '+', '1', '7' });
    const auto* vec = std::any_cast<std::vector<char>>(&storage);
    std::for_each(vec->cbegin(), vec->cend(), 
                  [](char c) { std::cout << c; });
    std::cout << '\n';
}

Output:

CelestialBody::CelestialBody(string, int)
CelestialBody{ "Sirius" : 2845 }
C++17

reset

Destroys contained object, leaving the any empty.

swap

Exchanges contents between two std::any instances.

Observer Functions

has_value

Returns true if the any contains a value:

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

int main() {
    std::boolalpha(std::cout);

    std::any empty;
    std::cout << "empty.has_value(): " << empty.has_value() << "\n";

    std::any num = 42;
    std::cout << "num.has_value(): " << num.has_value() << '\n';
    std::cout << "num = " << std::any_cast<int>(num) << '\n';
    num.reset();
    std::cout << "after reset, has_value(): " << num.has_value() << '\n';

    auto txt = std::make_any<std::string>("Andromeda");
    std::cout << "txt.has_value(): " << txt.has_value() << '\n';
    std::cout << "txt = \"" << std::any_cast<std::string&>(txt) << "\"\n";
    txt.reset();
    std::cout << "after reset, has_value(): " << txt.has_value() << '\n';
}

Output:

empty.has_value(): false
num.has_value(): true
num = 42
after reset, has_value(): false
txt.has_value(): true
txt = "Andromeda"
after reset, has_value(): false

type

Returns std::type_info of contained type, or typeid(void) if empty:

#include <type_traits>
#include <any>
#include <functional>
#include <iomanip>
#include <iostream>
#include <typeindex>
#include <typeinfo>
#include <unordered_map>
#include <vector>

template<class T, class F>
inline std::pair<const std::type_index, std::function<void(std::any const&)>>
    make_handler(F const& f) {
    return {
        std::type_index(typeid(T)),
        [g = f](std::any const& a) {
            if constexpr (std::is_void_v<T>)
                g();
            else
                g(std::any_cast<T const&>(a));
        }
    };
}

static std::unordered_map<
    std::type_index, std::function<void(std::any const&)>>
    dispatcher {
        make_handler<void>([]{ std::cout << "{empty}"; }),
        make_handler<int>([](int x){ std::cout << x; }),
        make_handler<unsigned>([](unsigned x){ std::cout << x; }),
        make_handler<float>([](float x){ std::cout << x; }),
        make_handler<double>([](double x){ std::cout << x; }),
        make_handler<char const*>([](char const* s)
            { std::cout << std::quoted(s); }),
    };

inline void dispatch(const std::any& a) {
    if (const auto it = dispatcher.find(std::type_index(a.type()));
        it != dispatcher.cend()) {
        it->second(a);
    } else {
        std::cout << "Unhandled type " << std::quoted(a.type().name());
    }
}

template<class T, class F>
inline void register_handler(F const& f) {
    std::cout << "Registered handler for "
              << std::quoted(typeid(T).name()) << '\n';
    dispatcher.insert(make_handler<T>(f));
}

int main() {
    std::vector<std::any> collection { {}, 100, 255u, 2.71828f, 
                                        1.41421, "π≈3.14159" };

    std::cout << "Collection contents: { ";
    for (const std::any& item : collection) {
        dispatch(item);
        std::cout << ", ";
    }
    std::cout << "}\n";

    dispatch(std::any(0xCAFEULL)); // unregistered type
    std::cout << '\n';

    register_handler<unsigned long long>([](auto x) {
        std::cout << std::hex << std::showbase << x; 
    });

    dispatch(std::any(0xCAFEULL)); // now registered: 0xcafe
    std::cout << '\n';
}

Output:

Collection contents: { {empty}, 100, 255, 2.71828, 1.41421, "π≈3.14159", }
Unhandled type "m"
Registered handler for "m"
0xcafe

Tags: C++ std::any type-erasure C++17 type-safety

Posted on Fri, 08 May 2026 00:20:08 +0000 by saint4