Introduction to std::any in C++
What Is std::any
std::any
is a new feature that comes with C++ 17 standard. It’s kind of void*
with type-safety, supporting copy/move/store/get, etc. Some basic usages of it can be found here: std::any - cppreference
Understanding how std::any
is implemented can be gainful, for it taking advantage of many c++ skills, especially templates.
Implementation of std::any
The source code referred to below is
Class Layout
std::any
has two data members:
__h_: _HandleFunPtr
, it’s a function pointer that points to a static function, as well as the entry of data manipulation including constructing/destroying/copying/access, etc. Prototype of the function is:1
2
3
4
5
6
7
8
9/*******
* arg1: Enum, kind of the manipulation, (_Destroy, _Copy, _Move, _Get, _TypeInfo)
* arg2: Caller's "this" pointer
* arg3: Destination any, used in copy, move
* arg4: Runtime type info, always nullptr if rtti is disabled
* arg5: Fallback type info, described in next chapter
*******/
using _HandleFuncPtr = void* (*)(_Action, any const *, any *, const type_info *,
const void* __fallback_info);__s_: _Storage
, stores pointer to managing data. It’s declared as a union for separately handling large and small objects.
In conclusion, std::any
is basically equal to an aggregate of a data block and a predefined manipulation function, which proves the famous saying Algorithms + Data Structures = Programs to some extent.
Skills of Implementation
Small objects optimization
Implementations are encouraged to avoid dynamic allocations for small objects. – cpp refrence
The data pointer __s_
is not a void*
but privately declared as a such union:
1 | using _Buffer = aligned_storage_t<3*sizeof(void*), alignment_of<void*>::value>; |
In a 64-bit machine, _Storage
occupies 24 bytes. __buf
is equally a void*
, used when the contained object is no larger than 24 bytes. Utilizing the benefits of stack memory, constructing or copying these small objects could be more effective. Larger objects, on the other hand, have to be stored on heap memory and allocated dynamically in runtime.
“24 bytes” is a curated threshold that is exactly the size of std::vector
/std::string
and many other STL containers in libcxx. This fact means std::any
can manage these common objects faster, though the memory inside them could still be dynamic.
A similar memory optimization technology is also applied on std::string
, but subtler. I will introduce it in the future.
In-place Construct
When a std::any
object is copied, the object managed by it is also copied. It’s pretty straight yet important, simply memcpy
is not enough because some classes have essential things to do, such as std::shared_ptr
. std::any
s copy object with its own copy constructor through allocator. It also applies to move construction.
But what about the constructor itself? Like emplace_back
for std::vector
, std::any
also has an emplace-like constructor, in which the object is directly constructed on __buf
instead of constructing a temporary object and then moving it.
1 | std::any a(std::in_place_type<std::string>, "hello"); |
By inspecting these 2 variables in a debugger, we can acknowledge that std::in_place_type<std::string>
has two folder meanings, it tells std::any
constructing a std::string
instead of a const char*
and constructing it directly.
Type to int mapping
Obviously, std::any
is not a template class itself, but it can throw exceptions when casting it to a different static type even if RTTI(run-time type info) is disabled. The secret of this type-safety is a mapping from type to an integer.
On line 162 of any.h
, a type-unique template struct is defined as:
1 | template <class _Tp> |
The static member __id
of __unique_typeinfo
is always equal to 0 but is a unique instance corresponding to type __Tp
due to template specialization. Based on this, std::any
gets the address of __id
as a fallback type id if RTTI is disabled (compiling with flag -fno-rtti
).
Best Practices
In conclusion, the best practices of std::any
include:
- To avoid unnecessary copying, use
std::make_any
orstd::in_place_type
to construct. - Pass
std::any
by reference if possible. - Use pointer version
std::any_cast<T>(&a)
to avoid copying large objects. - Let custom objects conform to the rule of three/five/zero if managed by
std::any
.
Formatting of std::any
in LLDB
Both belonging to Project LLVM, LLDB does not provide a formatted display of std::any
of libcxx (while GDB does with libstdc++). Printing std::any
in LLDB CLI will get:
1 | (lldb) n |
It takes seconds to understand “a” is an int (from _SmallHandler’s type) and its value is 0x1 (from first 4 bytes of __buf
). I write a Python script as a plugin based on LLDB API to print it more intuitively.
Implementation of plugin
There are 2 functions inside this script. __lldb_init_module
is the entry of this plugin. handle_std_any
is the processing handle used by LLDB.
A tricky skill is catching the type name inside _SmallHandler
using regex. After knowing that, we can find the object representing this type in python and then forcibly convert the pointer of buffer to it. This script should work for integers, floats, and std::string
, but not very robust now. I will continually polish it.
The same type-catching trick can also be employed in c++ source code.
__PRETTY_FUNCTION__
macro carries type name of a template function, so you can do some static reflections with it. A famous example is Magic Enum.
Another noticeable thing is that public classes of libcxx need special treatment because they have an inline namespace __1
.