Хотите получить представление о том, как устроен 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;
}
Автор: rpz