Идиомы С++. Type erasure

от автора

Хотите получить представление о том, как устроен boost::function, boost::any “под капотом”? Узнать или освежить в памяти, что скрывается за непонятной фразой “стирание типа”? В этой статье я постараюсь кратко изложить мотивацию, стоящую за этой идиомой и ключевые элементы реализации.

Мотивация

Как положить в один контейнер объекты никак не связанных друг с другом типов? Например, прочитанные из командной строки опции сразу “разложить” по разным типам и положить в единый контейнер. Или хранить внутри одного объекта “нечто” произвольного типа с единственным ограничением — наличием оператора “()” у хранимого “нечто”? Как, в общем случае, “стереть” тип любого объекта, скрыв его за объектом другого, некоего общего типа?

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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *