...

среда, 25 декабря 2013 г.

Идиомы С++. 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;
}


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий