Мотивация
Как положить в один контейнер объекты никак не связанных друг с другом типов? Например, прочитанные из командной строки опции сразу “разложить” по разным типам и положить в единый контейнер. Или хранить внутри одного объекта “нечто” произвольного типа с единственным ограничением — наличием оператора “()” у хранимого “нечто”? Как, в общем случае, “стереть” тип любого объекта, скрыв его за объектом другого, некоего общего типа?
void*
На самом деле в С++ есть встроенный механизм, позволяющий скрыть тип любого объекта за общим типом. Это — доставшийся в наследство от С, указатель void*.
Его можно использовать, например, так:
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);
Или так:
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;
Такая схема будет работать, но, думаю, её недостатки очевидны. Можно ошибиться при касте, передать неверный размер в конструктор, нельзя использовать с rvalue выражениями. Мы заставляем пользователя помнить о том, объект какого именно типа хранится в указателе и “вручную” приводить к этому типу. Ну а самое главный недостаток, пожалуй, в том, что мы никак не используем систему типов языка на котором пишем. Все равно что забивать гвоздь шуруповертом. Можно, но неудобно. Так как же быть?
Шаблоны и наследование
Вы уже наверное догадались, что без шаблонов здесь не обойдется. Да, действительно, в конструктор шаблонного класса (шаблонную функцию) можно передать объект любого типа и, тем самым, скрыть его тип, но этим мы не решим второй проблемы, а именно, скрыть объект любого типа за объектом одного общего типа.
template <typename T> struct some_t{}; some_t<int> s1; some_t<double> s2;
Во фрагменте выше s1 и s2 после инстанциирования являются объектами абсолютно разных, несвязанных типов.
К счастью, С++ не ограничивается одними шаблонами. И нам на помощь придет наследование и динамический полиморфизм. Читайте следующий раздел, чтобы понять как именно.
Реализация
Итак, от слов к делу. Нам уже ясно, что наша “обертка” не должна быть шаблоном, но при этом должна быть способна в конструкторе принять объект любого типа. Как это возможно? Правильно, с помощью шаблонного конструктора.
class any { public: template<typename T> any(const T& t); //… };
Но как теперь сохранить то, что нам передали в конструкторе? Наш класс ничего не знает о типе Т, параметризующем конструктор, поэтому так написать мы не можем:
class any { //... private: T t_; };
Для решения этой проблемы мы будем хранить указатель на абстрактную вспомогательную структуру, а переданное нам в конструкторе t, отдадим в структуру-шаблон, наследующую от абстрактной вспомогательной базы.
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_; };
Отлично! Теперь мы можем сохранить объект любого типа в классе “any”. Дело за малым, теперь сохраненный объект надо при необходимости каким-то образом “достать” из недр нашей обертки. Для этого, к сожалению, нам придется воспользоваться RTTI. Добавим функцию, возвращающую информацию о типе хранимого значения в наши вспомогательные структуры.
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_); } };
Теперь написать функцию возвращения исходного объекта не составит большого труда.
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_; }
Почему RTTI нужно использовать к сожалению? Потому что, хотелось бы написать что-то вроде такого, чтобы перенести проверку типа в 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_; }
Почему такое решение не подходит? Дело в том, что
std::is_same<U, decltype(static_cast<holder<U>* >(held_)->t_)>::value
всегда будет true, независимо от того какой на самом деле тип объекта, хранящегося в holder. Такой код будет компилироваться и даже выполняться без падений (если повезет)
any a(2); a.cast<std::string>();
Но результаты будут совсем не те, что ожидает программист.
В классе boost::function используется тот же принцип стирания типа. Косметические отличия заключаются в том, что function — шаблон, параметризуемый типами возвращаемого значения и аргументов, а во вспомогательных структурах появляется функция
virtual return_type operator()(arg_type1, .., arg_typeN);
Листинг
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; }
ссылка на оригинал статьи http://habrahabr.ru/post/207294/
Добавить комментарий