Want to get an idea of how
boost :: function ,
boost :: any is arranged “under the hood”? Learn or refresh what is hidden behind the incomprehensible phrase “type erasure”? In this article I will try to summarize the motivation behind this idiom and the key elements of implementation.
Motivation
How to put objects of unrelated types in one container? For example, the options read from the command line are immediately “decomposed” into different types and put into a single container. Or to store “something” of an arbitrary type inside a single object with the only restriction - the presence of the “()” operator in the stored “something”? How, in general, “to erase” the type of any object, hiding it behind an object of another, some general type?
void *
In fact, C ++ has a built-in mechanism for hiding the type of any object behind a common type. This is inherited from C, pointer
void * .
It can be used, for example, as follows:
struct A{ void foo(); }; struct B{ int bar(double); }; A a; B b; std::vector<void*> v; v.push_back(&a); v.push_back(&b); static_cast<A*>(v[0])->foo(); static_cast<B*>(v[1])->bar(3.5);
')
Or so:
class void_any { public: void_any(const void* h, size_t size) : size_(size) { h_ = std::malloc(size); std::memcpy(h_, h, size); } void get(void*& h) { h = std::malloc(size_); std::memcpy(h, h_, size_); } ~void_any(){ std::free(h_); } private: size_t size_; void* h_; }; int some_int=675321; void_any va(&some_int, sizeof(int)); void* pi; va.get(pi); std::cout << *(int*)pi << std::endl;
This scheme will work, but I think its drawbacks are obvious. You can make a mistake when casting, pass an incorrect size to the constructor, you cannot use it with
rvalue expressions. We make the user remember what type of object is stored in the pointer and “manually” lead to this type. Well, the main drawback, perhaps, is that we do not use the type system of the language in which we are writing. It's like nailing with a screwdriver. It is possible, but uncomfortable. So how to be?
Patterns and Inheritance
You've probably already guessed that without templates there is no cost. Yes, indeed, it is possible to transfer an object of any type to the template class constructor (template function) and thus hide its type, but this will not solve the second problem, namely, hide an object of any type behind an object of the
same type.
template <typename T> struct some_t{}; some_t<int> s1; some_t<double> s2;
In the fragment above,
s1 and
s2 after instantiation are objects of completely different, unrelated types.
Fortunately, C ++ is not limited to templates alone. And inheritance and dynamic polymorphism will come to our aid. Read the next section to understand how.
Implementation
So, from words to business. It is already clear to us that our “wrapper” should not be a template, but at the same time it should be capable of accepting an object of any type in the constructor. How is this possible? Correctly, with the help of the template designer.
class any { public: template<typename T> any(const T& t);
But now how to keep what they gave us in the constructor? Our class does not know anything about type T, which parameterizes the constructor, so we cannot write like this:
class any {
To solve this problem, we will store a pointer to an abstract auxiliary structure, and transferred to us in the constructor
t , we give to the template structure that inherits from the abstract auxiliary base.
class any { public: any(const T& t) : held_(new holder<T>(t)){}
Fine! Now we can save an object of any type in the
“any” class. The matter is small, now the saved object must, if necessary, be somehow “pulled out” from the depths of our wrapper. To do this, unfortunately, we will have to use
RTTI . Add a function that returns information about the type of stored value in our auxiliary structures.
struct base_holder {
Now write the function of returning the original object will not be difficult.
template<typename U> U cast() const { if(typeid(U) != held_->type_info()) throw std::runtime_error("Bad any cast"); return static_cast<holder<U>* >(held_)->t_; }
Why
RTTI need to be used unfortunately? Because I would like to write something like this to transfer the type check to compile time:
U cast(typename std::enable_if<std::is_same<U, decltype( static_cast<holder<U>* >(held_)->t_)>::value>::type* = 0) const { return static_cast<holder<U>* >(held_)->t_; }
Why is this solution not suitable? The fact is that
std::is_same<U, decltype(static_cast<holder<U>* >(held_)->t_)>::value
will always be
true , regardless of what type of object is actually stored in
holder . Such code will be compiled and even run without crashes (if lucky)
any a(2); a.cast<std::string>();
But the results will not be what the programmer expects.
The
boost :: function class uses the same type erase principle. Cosmetic differences lie in the fact that
function is a template parameterized by the types of the return value and arguments, and a function appears in auxiliary structures
virtual return_type operator()(arg_type1, .., arg_typeN);
Listing
class any { public: template<typename T> any(const T& t) : held_(new holder<T>(t)){} ~any(){ delete held_; } template<typename U> U cast() const { if(typeid(U) != held_->type_info()) throw std::runtime_error("Bad any cast"); return static_cast<holder<U>* >(held_)->t_; } private: struct base_holder { virtual ~base_holder(){} virtual const std::type_info& type_info() const = 0; }; template<typename T> struct holder : base_holder { holder(const T& t) : t_(t){} const std::type_info& type_info() const { return typeid(t_); } T t_; }; private: base_holder* held_; }; int main() { any a(2); std::cout << a.cast<int>() << std::endl; any b(std::string("abcd")); try { std::cout << b.cast<double>() << std::endl; } catch(const std::exception& e) { std::cout << e.what() << std::endl; } return 0; }