вторник, 15 марта 2016 г.

[prog.c++11] Этюд для программистов: методы базового класса должны возвращать ссылки на производный класс

Disclaimer: все написанное ниже будет очевидно для C++ных гуру, которые хорошо знакомы с CRTP. Однако пост писался не для гуру, а для самого себя, можно сказать, "в склерозник". Но если читатель, как и я сам, сталкивается с CRTP не чаще пары раз в году, то такому читателю может быть интересно вспомнить про CRTP еще раз и увидеть одну из ситуаций, в которых CRTP сильно выручает.

Встретился с интересной задачкой. Нужно создать несколько очень похожих друг на друга классов приблизительно вот такого вида:

class first_op_params_t
{
   type_A m_A;
   type_B m_B;
   ...
   type_N m_N;
public :
   type_A get_A() const { return m_A; }
   first_op_params_t & set_A(type_A v) { m_A = v; return *this; }

   type_B get_B() const { return m_B; }
   first_op_params_t & set_B(type_B v) { m_B = v; return *this; }
   ...
   type_N get_N() const { return m_N; }
   first_op_params_t & set_N(type_N v) { m_N = v; return *this; }
};

class second_op_params_t
{
   type_A m_A;
   type_B m_B;
   ...
   type_K m_K;
public :
   type_A get_A() const { return m_A; }
   second_op_params_t & set_A(type_A v) { m_A = v; return *this; }

   type_B get_B() const { return m_B; }
   second_op_params_t & set_B(type_B v) { m_B = v; return *this; }
   ...
   type_K get_K() const { return m_K; }
   second_op_params_t & set_K(type_K v) { m_K = v; return *this; }
};

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

Если бы методы-сеттеры возвращали void, то никаких проблем бы не было. Но они возвращают ссылки на конкретный тип, что позволяет объединять вызовы сеттеров в цепочки:

second_op_params_t{}.set_A(...).set_B(...).set_C(...);

Это означает, что есть появится класс basic_op_params_t, от которого будут наследоваться first_op_params_t и second_op_params_t, то метод, скажем, basic_op_params_t::set_A() должен знать, следует ли ему возвращать ссылку на first_op_params_t или на second_op_params_t.

Просто так возвращать ссылку на basic_op_params_t из basic_op_params_t::set_A() нельзя. Ведь тогда не получится сделать цепочку из вызовов set_SomeParam, если в этой цепочке будут перемешаны методы из basic_op_params_t и из наследника basic_op_params_t. (Надеюсь, понятно почему. Если нет, то я могу пояснить в комментариях).

Отсюда напрашивается вывод о том, что нужно применить технику Curiously recurring template pattern.

При использовании CRTP класс basic_op_params_t становится шаблоном. А параметром шаблона будет производный от basic_op_params_t класс! Т.е. у нас будет что-то вроде:

templatetypename D > class basic_op_params_t { ... };

class first_op_params_t : public basic_op_params_t< first_op_params_t > { ... };
class second_op_params_t : public basic_op_params_t< second_op_params_t > { ... };

Вот такая вот хитрая конструкция, с непривычки выворачивающая мозг. Тем не менее, если к ней привыкнуть, то жить вполне себе можно.

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

templatetypename D > class basic_op_params_t
{
   ...
   D & set_A(type_A v) { m_A = v; return static_cast<D &>(*this); }
};

Ну а вот минимальный самодостаточный пример решения этой задачки. С одним маленьким изменением относительно вышесказанного: дабы не писать static_cast постоянно, эта операция вынесена в отдельный метод self_reference:

templatetypename D >
class basic_op_params_t
{
   int m_a = { 0 };
   int m_b = { 0 };

protected :
   D & self_reference() { return static_cast<D &>(*this); }

public :
   int a() const { return m_a; }

   D & set_a(int v) { m_a = v; return self_reference(); }

   int b() const { return m_b; }

   D & set_b(int v) { m_b = v; return self_reference(); }
};

class first_op_params_t final : public basic_op_params_t< first_op_params_t >
{
   int m_c;

public :
   first_op_params_t( int c ) : m_c{ c } {}

   int c() const { return m_c; }

   first_op_params_t & set_c(int v) { m_c = v; return self_reference(); }
};

class second_op_params_t final : public basic_op_params_t< second_op_params_t >
{
   int m_d;

public :
   second_op_params_t( int d ) : m_d{ d } {}

   int d() const { return m_d; }

   second_op_params_t & set_d(int v) { m_d = v; return self_reference(); }
};

void op( first_op_params_t const & sp ) {}

first_op_params_t make_first(int c) { return first_op_params_t{c}; }

void op( second_op_params_t const & sp ) {}

second_op_params_t make_second(int d) { return second_op_params_t{d}; }

int main()
{
   op( make_first(0).set_a(2).set_c(4).set_b(3) );
   op( make_second(0).set_a(2).set_d(4).set_b(3) );
}

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