P4013R0: constexpr any of all the things
This paper is making std::any usable during constant evaluation.
Motivation
Often you just need to pass a value around, but not interact with it, for that in C++ you have multiple options: template (and keep the type information intact), inheritance (and being force to use intrusive inheritance in your types), passing allocated (const) void * pointer (and managing its lifetime), alternatively use std::unique_ptr or std::shared_ptr with custom deleters, or use a polymorphism (since C++26 we also got std::polymorphic).
Each of this option has ups and downs: templates has instantiation and huge cost of AST copying (and in growing executable size), inheritance has virtual tables. But turns out all these already are constexpr compatible. Standard already provides a facility to pass value based anything std::any, and in constant evaluator an allocation costs approx. same as normal values (at least in clang, anything bigger than int is an allocation, even pointer itself can be an allocation). Using std::any in code makes it obvious it's anything and makes code readable by not forcing to thing who owns the value, or how it is destroyed. Users shouldn't do such tradeoff, just because std::any is not marked constexpr.
Storing things in a vector
Let's pretend you want to store object collection of unrelated types in the prefered container.
Raw typeless pointer
std::vector<void *> things;
things.emplace_back(new int{42});
things.emplace_back(new std::string{"who owns me?"});
// you are on your own managing their lifetime, constant evaluation won't let you mess up, but it will be hard
Managing lifetime with unique_ptr
using any_unique_ptr = std::unique_ptr<void, Deleter>;
std::vector<any_unique_ptr> things;
things.emplace_back(any_unique_ptr{new int{42}, +[](int * ptr) { delete ptr; }});
things.emplace_back(any_unique_ptr{new std::string{"I drag this thing around"}, +[](std::string * ptr) { delete ptr; }});
// at least you don't need to manage the lifetime on your own
shared_ptr
std::vector<std::shared_ptr<void>> things;
things.emplace_back(std::static_pointer_cast<void>(std::make_shared_ptr<int>(42)));
things.emplace_back(std::static_pointer_cast<void>(std::make_shared_ptr<std::string>("a helper function will make this nicer")));
// since C++26 this is constexpr
templates and std::polymorphic
You must combine these two together :)
struct custom_any {
virtual ~base() = default;
};
template <typename T> struct specific_any: custom_any {
T value;
constexpr specific_any(Args && ... args): value{std::forward<Args>(args)...} { }
virtual ~specific_any() = default;
};
template <typename T> specific_any(T) -> specific_any<T>;
// we can't do this, but we can change language to do it:
// template <typename T> custom_any(T) -> specific_any<T>;
using polymorphic_any = std::polymorphic<custom_any>;
std::vector<polymorphic_any> things;
things.emplace_back(polymorphic_any{std::in_place_type<int>, 42});
things.emplace_back(polymorphic_any{std::in_place_type<std::string>, "I wrote my own any yay"});
// you can also write your own `std::any` directly
std::any
std::vector<std::any> things;
things.emplace_back(42);
things.emplace_back("it's almost like javascript :)");
// after this paper, this will work in constexpr too
Changes
Every standard library implementation sets a metadata type (a custom vtable) which contains methods how to destroy / copy / move specific type in std::any. Selection which type is used is done typically via a series of conditional statement, querying triviallity of the type, size of the type. This paper can be implemented trivially by prepending this sequence with a if consteval statement which will pick the allocating behavior for every type.
Fortunetely all this changes can happen inside current specification, and we just need to sprinkle constexpr keyword in the spec. That's all the change
ABI and impact
This change doesn't break ABI, is fully compatible with existing code. And make std::any not being able to leave constant evaluated code, unless the language gets non-transient constexpr allocations. And even then this will be compatible, only the constexpr variable containing value stored inside std::any would have a non-transient allocation inside instead of a short buffer inlined value.
Implementations
libc++
As described in changes everything is marked constxpr and call of _Handler::__create is dispatch thru a helper function which uses if consteval and force allocation. All other functionality is using pointer to the handler put there by the _LargeHandler::__create function. Which was also changed to use operator new instead of libc++ allocating function __libcpp_allocate. Change is visible on my github and available on compiler explorer under hana's clang.
libstdc++
Similar change as in libc++ but without a wrapping create function. Plus any_cast is a bit more brittle in libc++, and compares function pointer, instead of calling the access itself which should do the check if it's compatible type. Code is visible on my github.
MS STL
Microsoft's STL is using extensively reinterpret_cast for pointer tagging, to store const type_info * and type of storage (trivial / small buffer optimization / allocation). To avoid usage of these I used prototype of pointer_tagging from P3125 which does same functionality but it works also during constant evaluation. Another thing which needed to change is due the disrepancy between type and storage is to actually look into the tagged pointer, how the payload is storage and use that approach to access it, instead of blindly going in. Code is available on my github.
Wording
22.7.1 General [any.general]
22.7.2 Header <any> synopsis [any.synop]
22.7.3 Class bad_any_cast [any.bad.any.cast]
constexpr const char* what() const noexcept override;
22.7.4 Class any [any.class]
22.7.4.1 General [any.class.general]
22.7.4.2 Construction and destruction [any.cons]
constexpr any() noexcept;
constexpr any(const any& other);
constexpr any(any&& other) noexcept;
template<class T>
constexpr any(T&& value);
template<class T, class... Args>
constexpr explicit any(in_place_type_t<T>, Args&&... args);
template<class T, class U, class... Args>
constexpr explicit any(in_place_type_t<T>, initializer_list<U> il, Args&&... args);
constexpr ~any();
22.7.4.3 Assignment [any.assign]
constexpr any& operator=(const any& rhs);
constexpr any& operator=(any&& rhs) noexcept;
template<class T>
constexpr any& operator=(T&& rhs);
22.7.4.4 Modifiers [any.modifiers]
template<class T, class... Args>
constexpr decay_t<T>& emplace(Args&&... args);
template<class T, class U, class... Args>
constexpr decay_t<T>& emplace(initializer_list<U> il, Args&&... args);
constexpr void reset() noexcept;
constexpr void swap(any& rhs) noexcept;
22.7.4.5 Observers [any.observers]
constexpr bool has_value() const noexcept;
constexpr const type_info& type() const noexcept;
22.7.5 Non-member functions [any.nonmembers]
constexpr void swap(any& x, any& y) noexcept;
template<class T, class... Args>
constexpr any make_any(Args&&... args);
template<class T, class U, class... Args>
constexpr any make_any(initializer_list<U> il, Args&&... args);
template<class T>
constexpr T any_cast(const any& operand);
template<class T>
constexpr T any_cast(any& operand);
template<class T>
constexpr T any_cast(any&& operand);
template<class T>
constexpr const T* any_cast(const any* operand) noexcept;
template<class T>
constexpr T* any_cast(any* operand) noexcept;
Feature test macro
17.3.2 Header <version> synopsis [version.syn]
#define __cpp_lib_any 201606L2026??L // also in <any>