Хотите получить представление о том, как устроен boost::function, boost::any “под капотом”? Узнать или освежить в памяти, что скрывается за непонятной фразой “стирание типа”? В этой статье я постараюсь кратко изложить мотивацию, стоящую за этой идиомой и ключевые элементы реализации.
Как положить в один контейнер объекты никак не связанных друг с другом типов? Например, прочитанные из командной строки опции сразу “разложить” по разным типам и положить в единый контейнер. Или хранить внутри одного объекта “нечто” произвольного типа с единственным ограничением — наличием оператора “()” у хранимого “нечто”? Как, в общем случае, “стереть” тип любого объекта, скрыв его за объектом другого, некоего общего типа?
На самом деле в С++ есть встроенный механизм, позволяющий скрыть тип любого объекта за общим типом. Это — доставшийся в наследство от С, указатель void*.
Его можно использовать, например, так:
Или так:
Такая схема будет работать, но, думаю, её недостатки очевидны. Можно ошибиться при касте, передать неверный размер в конструктор, нельзя использовать с rvalue выражениями. Мы заставляем пользователя помнить о том, объект какого именно типа хранится в указателе и “вручную” приводить к этому типу. Ну а самое главный недостаток, пожалуй, в том, что мы никак не используем систему типов языка на котором пишем. Все равно что забивать гвоздь шуруповертом. Можно, но неудобно. Так как же быть?
Вы уже наверное догадались, что без шаблонов здесь не обойдется. Да, действительно, в конструктор шаблонного класса (шаблонную функцию) можно передать объект любого типа и, тем самым, скрыть его тип, но этим мы не решим второй проблемы, а именно, скрыть объект любого типа за объектом одного общего типа.
Во фрагменте выше s1 и s2 после инстанциирования являются объектами абсолютно разных, несвязанных типов.
К счастью, С++ не ограничивается одними шаблонами. И нам на помощь придет наследование и динамический полиморфизм. Читайте следующий раздел, чтобы понять как именно.
Итак, от слов к делу. Нам уже ясно, что наша “обертка” не должна быть шаблоном, но при этом должна быть способна в конструкторе принять объект любого типа. Как это возможно? Правильно, с помощью шаблонного конструктора.
Но как теперь сохранить то, что нам передали в конструкторе? Наш класс ничего не знает о типе Т, параметризующем конструктор, поэтому так написать мы не можем:
Для решения этой проблемы мы будем хранить указатель на абстрактную вспомогательную структуру, а переданное нам в конструкторе t, отдадим в структуру-шаблон, наследующую от абстрактной вспомогательной базы.
Отлично! Теперь мы можем сохранить объект любого типа в классе “any”. Дело за малым, теперь сохраненный объект надо при необходимости каким-то образом “достать” из недр нашей обертки. Для этого, к сожалению, нам придется воспользоваться RTTI. Добавим функцию, возвращающую информацию о типе хранимого значения в наши вспомогательные структуры.
Теперь написать функцию возвращения исходного объекта не составит большого труда.
Почему RTTI нужно использовать к сожалению? Потому что, хотелось бы написать что-то вроде такого, чтобы перенести проверку типа в compile time:
Почему такое решение не подходит? Дело в том, что
всегда будет true, независимо от того какой на самом деле тип объекта, хранящегося в holder. Такой код будет компилироваться и даже выполняться без падений (если повезет)
Но результаты будут совсем не те, что ожидает программист.
В классе boost::function используется тот же принцип стирания типа. Косметические отличия заключаются в том, что function — шаблон, параметризуемый типами возвращаемого значения и аргументов, а во вспомогательных структурах появляется функция
Мотивация
Как положить в один контейнер объекты никак не связанных друг с другом типов? Например, прочитанные из командной строки опции сразу “разложить” по разным типам и положить в единый контейнер. Или хранить внутри одного объекта “нечто” произвольного типа с единственным ограничением — наличием оператора “()” у хранимого “нечто”? Как, в общем случае, “стереть” тип любого объекта, скрыв его за объектом другого, некоего общего типа?
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;
}