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 класс! Т.е. у нас будет что-то вроде:
template< typename 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 > { ... }; |
Вот такая вот хитрая конструкция, с непривычки выворачивающая мозг. Тем не менее, если к ней привыкнуть, то жить вполне себе можно.
Ну а раз тип наследника в виде параметра шаблона становится видимым в базовом классе, то появляется возможность сделать так, чтобы методы-сеттеры в базовом классе возвращали ссылку на наследника:
template< typename D > class basic_op_params_t { ... D & set_A(type_A v) { m_A = v; return static_cast<D &>(*this); } }; |
Ну а вот минимальный самодостаточный пример решения этой задачки. С одним маленьким изменением относительно вышесказанного: дабы не писать static_cast постоянно, эта операция вынесена в отдельный метод self_reference:
template< typename 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) ); } |
Комментариев нет:
Отправить комментарий