Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
template_metaprogramming_ru.docx
Скачиваний:
12
Добавлен:
01.07.2025
Размер:
774 Кб
Скачать
☆

Выбор структуры

Использование метафункций для настройки кложенных элементов классов/структур выглядит так:

template <class T>

struct X

{

static int m1 = metafunction1<T>::type::value;

typedef typename metafunction2<T>::type m2;

int m4(typename metafunction3<T>::type p);

//...

};

Так, вычисляется значение для m1, тип для m2 и тип параметра функции для m4.

Предположим, необходимо контролировать наличие м2 в той или иной специализации X. Подход, используемый выше позволяет нам управлять детали данного элементов класса, но фундаментальные структурные изменения требуют более мощной техники.

Выбор структуры содержит вставку модифицируемой части классовой структуры в простой открытый базовый класс или шаблоный базовоый класс и использование метапрограммы для выбора между ними. Чтобы увидеть, как это работает, давайте исправим проблемы в compose_fg:

template <class R, class F, class G>

class compose_fg

{

public:

compose_fg(F const& f, G const& g)

: f(f), g(g)

{}

template <class T>

R operator()(T const& x) const

{

return f(g(x));

}

private:

F f;

G g;

};

Первым решением было генерирование объекта следующим образом:

compose_fg<float,std::negate<float>,float(*)(float)>

так что F является std::negate<float>. В большинстве реализаций std:: negate содержит лишь операторную функцию осуществляющая инверсию своего аргумента:

T operator()(const T& x) const { return -x; }

Т.е., это пустой класс, т.к. в нем нет элементов данных. Хотя, стандарт C++ говорит, что каждый из элементов данных compose_fg должен занимать по крайней мере один байт. В типичной схеме класса компоновки, первый байт будет отведен для F f, хотя специализации negate не содержат элементов данных, ему необходимо несколько быйт (скажем, три) для выравнивания памяти под указатель на функции, и память для g (скажем, четыре байта), которая будет следовать после, т.о. объект занимает 8 байт памяти. Если не объявлять элементы данных класса, то вес compose_fg будет 1 байт и это предел, т.к. даже пустой класс должен иметь ненулевой размер.

Как таковых ограничения на расположение в памяти элементов данных класса нет, за исключением того, что они не могут занимать 1 адрес и перекрывать друг друга. Исключение: POD-класс, элементы данных располагаются в памяти более предсказуемо.

Одним из способов ликвидации хранения пустых базовых классов является их обнаружение (с помощью свойства boost::is_empty) и пропуск.

С этим подходом связано несколько проблем:

  • Отсутствие прозрачности: даже пустые классы могут иметь нетривиальные конструкторы и деструкторы, и если не хранить копии f и g, то различия в поведении compose_fg могут быть существенными.

  • Для реализации оператора() необходимы объекты F и G, если их не хранить, то в случае необходимости их нужно создавать, но они могут не содержать конструктора по умолчанию.

Лучшее решение: компиляторы могут осуществлять оптимизацию пустого базового класса (EBO – Empty Base Optimisation), которая позволяет разместить пустой базовый класс по адресу любого под-объекта данного класса, но, до тех пор пока этот адрес принадлежит лишь одному под-объекту данного типа.

Например:

compose_fg<float,std::negate<float>,float(*)(float)>

Функтор-композитор compose_fg может иметь идеальный размер, если будет организован так:

template <class R, class F, class G>

class compose_fg : F // if empty, F may overlap with g

{

public:

typedef R result_type;

compose_fg(F const& f, G const& g)

: F(f), g(g) // initialize base with f

{}

template <class T>

R operator()(T const& x) const

{

F const& f = *this; // retrieve F subobject

return f(g(x));

}

private:

G g;

};

Естественно, не получиться использовать эту структуру для всех специализаций compose_fg: если F будет указателем на функцию, то будет ошибка компиляции, поскольку указатели на функции не могут быть базовыми классами. Кроме того, эта структура не будет использоваться во всех случаях: когда G является пустым классом, а не F, то необходимо наследовать именно его. Потому необходим механизм выбора подходящей структуры в конкретный момент:

// base class template to be defined later

template <class F, bool F_empty, class G, bool G_empty> class storage;

template <class R, class F, class G>

class compose_fg

: storage<

F, boost::is_empty<F>::value,

G, boost::is_empty<G>::value

>

{

private:

typedef

storage<

F,boost::is_empty<F>::value,

G,boost::is_empty<G>::value

> base;

public:

compose_fg(F const& f, G const& g)

: base(f, g)

{}

template <class T>

R operator()(T const& x) const

{

F const& f = this->get_f();

G const& g = this->get_g();

return f(g(x));

}

};

Теперь написать класс storage так, чтобы он имел правильную структуру для каждой из четырех комбинаций F_empty и G_empty, и предоставлял доступ к сохраненному F и G посредством get_f и get_g:

//+--------------------------------------------------------------------+

// neither F nor G is empty

template <class F, class G>

class storage<F, false, G, false>

{

protected:

storage(F const& f, G const& g)

: f(f), g(g)

{}

F const& get_f() { return f; }

G const& get_g() { return g; }

private:

F f;

G g;

};

//+--------------------------------------------------------------------+

// G is empty

template <class F, class G>

class storage<F,false,G,true>

: private G

{

protected:

storage(F const& f, G const& g)

: G(g), f(f)

{}

F const& get_f() { return f; }

G const& get_g() { return *this; }

private:

F f;

};

//+--------------------------------------------------------------------+

// F is empty

template <class F, class G>

class storage<F,true,G,false>

: private F

{

protected:

storage(F const& f, G const& g)

: F(f), g(g)

{}

F const& get_f() { return *this; }

G const& get_g() { return g; }

private:

G g;

};

//+--------------------------------------------------------------------+

// F and G are both empty

template <class F, class G>

class storage<F,true,G,true>

: private F, private G

{

protected:

storage(F const& f, G const& g)

: F(f), G(g)

{}

F const& get_f() { return *this; }

G const& get_g() { return *this; }

};

Нет никаких гарантий, что конкретный компилятор будет выполнять автоматическую оптимизацию, но для тех, которые ее выполняют, данная структура обеспечит более эффективное использование элементов данных класса.

В библиотеке Boost есть шаблон compressed_pair, который обобщает механизм оптимизации EBO.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]