библиотека — типы общего назначенияlibrary — general-purpose types

std::any

Контейнер, хранящий значение любого типа, известного только в рантайме — типобезопасная альтернатива void*. В отличие от variant, набор возможных типов не фиксирован на этапе компиляции.A container holding a value of any type, known only at runtime — a type-safe alternative to void*. Unlike variant, the set of possible types isn't fixed at compile time.

крайняя мера, не первый выборa last resort, not a first choice Library Fundamentals TS v1 частый вопрос: почему не variantcommon question: why not variant

Что этоWhat it is

std::any — объект, способный хранить значение произвольного copy-constructible типа, определяемого динамически при присваивании. В отличие от std::variant<T1, T2, ...>, где набор допустимых типов перечислен в шаблонных параметрах, any не ограничен заранее заданным списком — в него можно положить значение любого типа в любой момент. Извлечение — через std::any_cast<T>(a), кидающий std::bad_any_cast при несовпадении реального типа, либо std::any_cast<T>(&a), возвращающий указатель или nullptr.std::any is an object capable of holding a value of any copy-constructible type, determined dynamically on assignment. Unlike std::variant<T1, T2, ...>, where the set of allowed types is listed as template parameters, any isn't limited to a predetermined list — you can put a value of any type into it at any moment. Extraction is via std::any_cast<T>(a), which throws std::bad_any_cast on a type mismatch, or std::any_cast<T>(&a), which returns a pointer or nullptr.

до C++17 — void* без типобезопасностиbefore C++17 — void* with no type safety
void* makeValue(int kind);

void* v = makeValue(1);
// static_cast<Widget*>(v) — компилятор ничего не проверяет,// static_cast<Widget*>(v) — the compiler checks nothing,
// перепутать реальный тип — UB, а не ошибка компиляции/исключение// getting the real type wrong is UB, not a compile error/exception
Widget* w = static_cast<Widget*>(v);
C++17 — std::anyC++17 — std::any
std::any makeValue(int kind);

std::any v = makeValue(1);
if (auto* w = std::any_cast<Widget>(&v)) {
    use(*w);  // безопасно — тип реально проверен в рантайме// safe — the type was actually checked at runtime
} // иначе w == nullptr, никакого UB// otherwise w == nullptr, no UB

Зачем ввелиWhy it was introduced

Гетерогенное хранение значений произвольного типа встречается в реальных задачах чаще, чем хотелось бы, и до C++17 единственным типобезопасным способом было заранее знать список типов:Heterogeneous storage of arbitrary-type values comes up in real tasks more often than one would like, and before C++17 the only type-safe way was to know the list of types ahead of time:

01

void* был единственным способом выразить «тип неизвестен на этапе компиляции»void* was the only way to express "type unknown at compile time"

Плагинные архитектуры, générик message passing, конфиги с произвольными по форме значениями — все они регулярно упирались в необходимость хранить значение, тип которого известен только вызывающему коду в конкретный момент, а не на этапе компиляции библиотеки. До C++17 это почти всегда решалось через void* с ручным приведением, полагаясь на дисциплину, а не на компилятор.Plugin architectures, generic message passing, configs with arbitrarily-shaped values — all of these regularly ran into the need to store a value whose type is known only to the calling code at a specific moment, not at the library's compile time. Before C++17 this was almost always solved via void* with manual casting, relying on discipline rather than the compiler.

02

any заменяет большинство void*-паттернов легаси-кода с сохранением типобезопасностиany replaces most void*-based legacy patterns while keeping type safety

Там, где легаси-код хранит void* и вручную приводит его к нужному типу без проверки, замена на std::any с any_cast добавляет проверку типа в рантайме почти бесплатно с точки зрения архитектуры — при несовпадении типа программа получает исключение или nullptr вместо тихого UB.Wherever legacy code stores a void* and manually casts it to the expected type without checking, replacing it with std::any plus any_cast adds a runtime type check almost for free architecturally — on a type mismatch, the program gets an exception or a nullptr instead of silent UB.

Как и optional, any унаследован из экспериментального Library Fundamentals TS v1 (2015) практически без изменения семантики — std::experimental::any в коде до C++17 ведёт себя почти идентично финальному std::any.Like optional, any was inherited from the experimental Library Fundamentals TS v1 (2015) with almost no change in semantics — std::experimental::any in pre-C++17 code behaves nearly identically to the final std::any.

Как это работает под капотомHow it works under the hood

any хранит указатель на type-erasure обёртку (обычно с виртуальными функциями clone/destroy/type-info внутри) плюс сам объект — либо инлайново в небольшом внутреннем буфере (small buffer optimization, для маленьких типов), либо в куче, выделенной через new (для крупных или не-noexcept-move типов). std::type_info, доступный через a.type(), позволяет узнать реальный хранимый тип в рантайме без попытки any_cast.any stores a pointer to a type-erasure wrapper (usually with virtual clone/destroy/type-info functions inside) plus the object itself — either inline in a small internal buffer (small buffer optimization, for small types), or on the heap allocated via new (for large or non-noexcept-movable types). std::type_info, available via a.type(), lets you learn the actually stored type at runtime without attempting an any_cast.

std::any a = 42;
std::cout << a.type().name();  // имя реального типа, обычно "i" для int (implementation-defined)// the real type's name, usually "i" for int (implementation-defined)

a = std::string("hello");  // смена типа — старое значение уничтожается, новое конструируется// type change — the old value is destroyed, the new one constructed

try {
    int x = std::any_cast<int>(a);  // кидает bad_any_cast — сейчас там string, не int// throws bad_any_cast — a string is stored now, not int
} catch (const std::bad_any_cast& e) {
    // корректная обработка несовпадения типа// proper handling of the type mismatch
}

if (a.has_value() && a.type() == typeid(std::string)) {
    auto& s = *std::any_cast<std::string>(&a);  // без исключений, указатель уже проверен по типу// no exceptions, the pointer's type was already verified
}

Важное следствие: хранение крупного или не-noexcept-перемещаемого типа внутри any почти всегда означает выделение в куче при каждом присваивании нового значения — small buffer optimization спасает только маленькие тривиально-перемещаемые типы (реализация-зависимо, но обычно что-то около размера указателя-двух). Это делает any заметно дороже variant, у которого размер объекта фиксирован статически и куча не задействуется никогда.An important consequence: storing a large or non-noexcept-movable type inside any almost always means a heap allocation on every new value assignment — small buffer optimization only helps small, trivially movable types (implementation-defined, but usually around one-to-two pointer widths). This makes any noticeably more expensive than variant, whose object size is fixed statically and which never touches the heap.

ГраблиGotchas

any почти всегда означает «архитектура продумана не до конца»any almost always means "the architecture wasn't fully thought through"

Если набор возможных типов известен заранее — это задача для variant, не any. Если нужен полиморфизм по общему интерфейсу — это задача для обычной виртуальной иерархии или шаблонов. any стоит рассматривать как крайнюю меру для действительно гетерогенной рантайм-типизации (плагинные системы, générик message passing между независимо скомпилированными модулями), а не как удобный способ обойти проектирование типов.If the set of possible types is known ahead of time, that's a job for variant, not any. If polymorphism over a common interface is needed, that's a job for an ordinary virtual hierarchy or templates. any should be treated as a last resort for genuinely heterogeneous runtime typing (plugin systems, generic message passing between independently compiled modules), not as a convenient way to skip type design.

any не бесплатен — обычно есть аллокация в кучеany isn't free — there's usually a heap allocation

В отличие от variant, размер и стоимость которого фиксированы и известны статически, any почти всегда платит цену динамической аллокации для любого типа за пределами small-buffer-порога (обычно это исключает даже такие "небольшие" типы, как большинство пользовательских struct из нескольких полей). Использование any в горячем пути (hot loop) — частая скрытая причина просадки производительности.Unlike variant, whose size and cost are fixed and known statically, any almost always pays the price of a dynamic allocation for any type beyond the small-buffer threshold (which typically excludes even "smallish" types like most multi-field user structs). Using any in a hot loop is a common hidden cause of performance regressions.

any_cast с несовпадающим типом — исключение, а не false/nullptr, если используешь версию по значениюany_cast with a mismatched type throws, rather than returning false/nullptr, when using the by-value overload

Как и с optional::value() vs operator*, у any_cast два разных контракта: std::any_cast<T>(a) (по значению) кидает std::bad_any_cast при несовпадении, а std::any_cast<T>(&a) (по указателю) просто возвращает nullptr. Использовать первую форму в цикле проверки «а вдруг там T» без try/catch — почти гарантированный краш при первом же несовпадении.Just like optional::value() vs operator*, any_cast has two different contracts: std::any_cast<T>(a) (by value) throws std::bad_any_cast on a mismatch, while std::any_cast<T>(&a) (by pointer) simply returns nullptr. Using the first form in a "maybe it's T" checking loop without try/catch is an almost guaranteed crash on the first mismatch.