Pull to refresh

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

Reading time 4 min
Views 47K
Хотите получить представление о том, как устроен 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;
}
Tags:
Hubs:
+32
Comments 25
Comments Comments 25

Articles