📜 ⬆️ ⬇️

C ++ idioms. Type erasure

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 { //... private: T t_; }; 

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)){} //… private: struct base_holder { virtual ~base_holder(){} }; template<typename T> struct holder : base_holder { holder(const T& t) : t_(t){} T t_; }; private: base_holder* held_; }; 

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 { //... virtual const std::type_info& type_info() const = 0; }; template<typename T> struct holder : base_holder { //... const std::type_info& type_info() const { return typeid(t_); } }; 

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; } 

Source: https://habr.com/ru/post/207294/


All Articles