понедельник, 10 июля 2017 г.

[prog.c++14] Развернуть std::tuple в вызов конструктора базового класса

Давеча упоролся шаблонами настолько, что потребовалось сделать на C++ что-то вот такое (проще сперва показать на примере, а уже потом рассказывать словами):

templatetypename First_base, typename Second_base >
class Some_complex_template
   : public First_base // Boom #1
   , public Second_base // Boom #2
{
public :
   // Constructor.
   templatetypename... First_base_args, typename... Second_base_args >
   Some_complex_template(
      First_base_args &&...first_args, // Args for the first base class.
      Second_base_args &&...second_args ) // Args for the second base class.
      : First_base{ std::forward<First_base_args>(first_args)... }
      , Second_base{ std::forward<Second_base_args>(second_args)... }
      {}
   ...
};

Т.е. нужно было отнаследовать шаблонный класс Some_complex_template от двух других классов, которые задаются параметрами шаблона. А затем в конструктор Some_complex_template нужно было передать два независимых друг от друга набора параметров. Первый набор параметров должен уйти в конструктор первого базового класса, второй набор -- в конструктор второго базового класса.

Насколько я понимаю, C++ не позволяет написать фукнцию/метод с переменным количеством параметров вот так: f(First &&...first, Second &&...second), что логично, т.к. при вызове f(a1, a2, a3, a4, a5) невозможно понять, что из a(i) относится к first, а что к second.

Поэтому выход из ситуации сейчас ищется в использовании std::tuple вот в таком сценарии:

templatetypename First_base, typename Second_base >
class Some_complex_template
   : public First_base
   , public Second_base
{
public :
   // Constructor.
   templatetypename... First_base_args, typename... Second_base_args >
   Some_complex_template(
      std::tuple<First_base_args...> && first_args, // Args for the first base class.
      std::tuple<Second_base_args...> && second_args ) // Args for the second base class.
      : First_base{ /*some magic is required here!*/(first_args)... }
      , Second_base{ /*some magic is required here!*/(second_args)... }
      {}
   ...
};

Но вот тут возникает вопрос, как же распаковать содержимое std::tuple в вызов конструктора базового типа?

В принципе, вся эта магия с распаковкой std::tuple в вызов некоторой функции f хорошо проиллюстрирована на том же cppreferece.com в описании возможной реализации функции std::apply. Но есть нюанс: нам нужно сделать вызов конструктора базового типа, поэтому у нас нет возможности заводить вспомогательные функции, вроде apply_impl, в которые передается дополнительный аргумент std::index_sequence...

Или все-таки есть?

Все-таки есть :)

Мы легко можем воспользоваться делегирующими конструктором. Вот, для простоты иллюстрации пример для всего одного базового типа:

#include <tuple>
#include <utility>

#include <cassert>

struct A {
   int a_;
   float b_;

   A(int a, float b) : a_(a), b_(b) {}
};

templatetypename Base >
struct D : public Base {
   templatetypename... Items >
   D( int c, std::tuple<Items...> items )
      : D{ c, std::move(items), std::make_index_sequence<sizeof...(Items)>{} }
      {}

   templatetypename Tuple, std::size_t... Indexes  >
   D(int c, Tuple && items, std::index_sequence<Indexes...>)
      : Base{ std::get<Indexes>(std::forward<Tuple>(items))... }
      , c_{c}
      {}

   int c_;
};

int main() {
   D<A> d(0, std::make_tuple(10.2f));

   assert( 0 == d.c_ );
   assert( 1 == d.a_ );
}

Здесь фокус в том, что у структуры D есть два конструктора. Первый -- нормальный, который должен использоваться пользователем. Первый конструктор получает std::tuple. А вот второй конструктор вспомогательный. По хорошему, он не должен быть виден снаружи. И как раз второй конструктор уже получает полный набор аргументов для того, чтобы выполнить магию с std::get. Первый же конструктор просто делегирует всю работу второму конструктору.

Пример должен быть компилябельным, можно скопипастить и проверить его на каком-нибудь вменяемом C++14 компиляторе (я пробовал под gcc-5.2, clang-3.9, vc++14 и 15).

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

#include <tuple>
#include <utility>

#include <cassert>

struct A {
   int a_;
   float b_;

   A(int a, float b) : a_{a}, b_{b} {}
};

struct B0 {
   B0() {}
};

struct B1 {
   long b1_;

   B1(long b1) : b1_{b1} {}
};

templatetypename First_base, typename Second_base >
struct D
   : public First_base
   , public Second_base
{
   templatetypename... First_pack, typename... Second_pack >
   D(int c,
      std::tuple<First_pack...> && first_pack,
      std::tuple<Second_pack...> && second_pack )
      : D{ c,
         std::move(first_pack),
         std::make_index_sequence<sizeof...(First_pack)>{},
         std::move(second_pack),
         std::make_index_sequence<sizeof...(Second_pack)>{} }
      {}

   template<
      typename First_tuple,
      std::size_t... First_tuple_indexes,
      typename Second_tuple,
      std::size_t... Second_tuple_indexes >
   D(int c,
      First_tuple && first_pack,
      std::index_sequence<First_tuple_indexes...>,
      Second_tuple && second_pack,
      std::index_sequence<Second_tuple_indexes...> )
      : First_base{
            std::get<First_tuple_indexes>(
                  std::forward<First_tuple>(first_pack))... }
      , Second_base{
            std::get<Second_tuple_indexes>(
                  std::forward<Second_tuple>(second_pack))... }
      , c_{c}
      {}

   int c_;
};

int main() {
   D<A, B0> d(0, std::make_tuple(20.2f), std::make_tuple());

   assert( sizeof(A) + sizeof(int) == sizeof(d) );
   assert( 0 == d.c_ );
   assert( 2 == d.a_ );

   D<A, B1> d1(3, std::make_tuple(40.2f), std::make_tuple(5));

   assert( sizeof(A) + sizeof(int) + sizeof(long) == sizeof(d1) );
   assert( 3 == d1.c_ );
   assert( 4 == d1.a_ );
   assert( 5 == d1.b1_ );
}

Ну вот как-то так. Упарываться шаблонами, так упарываться :)

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