На RSDN-е задали любопытный вопрос. Я этот вопрос понял так:
У нас есть базовый класс A с неким виртуальным методом f. Пользователь создает наследников класса A, в которых он переопределяет f. Но нужно гарантировать, что в переопределенном f обязательно вызывается f из A.
Грубо говоря, вот так нормально:
class A { public: virtual void f() { std::cout << "A::f()" << std::endl; } }; class B : public A { public: void f() override { A::f(); std::cout << "B::f()" << std::endl; } }; |
а вот так уже нет, за такое нужно бить по рукам:
class A { public: virtual void f() { std::cout << "A::f()" << std::endl; } }; class C : public A { public: void f() override { std::cout << "C::f()" << std::endl; } }; |
В обсуждении на RSDN описано несколько решений, я же здесь остановлюсь на одном, которое лично мне кажется наиболее простым, понятным, работающим очевидным образом (и очевидно, что работающим), и гибким плюс ко всему.
Идея состоит в том, чтобы заставить f() возвращать экземпляр типа, который может создать только базовый класс. Соответственно, если f() переопределяется в производном классе, то этот экземпляр нужно где-то взять. А взять его можно только вызвав f() из базового класса.
Поэтому решение выглядит так (только публичный f() теперь не виртуальный, чтобы спрятать от пользователя всю эту внутреннюю машинерию, виртуальным будет f_impl()):
class A { protected: class F_completed { friend class A; F_completed() = default; public: }; [[nodiscard]] virtual F_completed f_impl() { std::cout << "A::f()" << std::endl; return {}; } public: void f() { std::ignore = f_impl(); } }; class B : public A { F_completed f_impl() override { auto r = A::f_impl(); std::cout << "B::f()" << std::endl; return r; } public: }; |
В таком подходе мы уже не можем забыть вызвать f_impl() из базового класса, т.к. из своего f_impl() нам нужно что-то вернуть, но это что-то взять неоткуда, кроме как из f_impl() базового класса.
Гибкость здесь в том, что в своем f_impl() мы вольны вызывать унаследованный f_impl() когда вздумается:
class B : public A { F_completed f_impl() override { auto r = A::f_impl(); std::cout << "B::f()" << std::endl; return r; } }; class C : public A { F_completed f_impl() override { std::cout << "C::f()" << std::endl; return A::f_impl(); } }; class D : public A { F_completed f_impl() override { std::cout << "D::f() 1" << std::endl; auto r = A::f_impl(); std::cout << "D::f() 2" << std::endl; return r; } }; |
Пока все было достаточно просто, т.к. A::f() возвращает void. А что делать, если A::f() должен возвращать, скажем, int?
Мне в голову приходит такой подход:
class A { protected: class G_completed { friend class A; int result_; G_completed(int result) : result_{result} {} public: [[nodiscard]] int value() const noexcept { return result_; } [[nodiscard]] G_completed replace_by(int value) const { return {value}; } }; [[nodiscard]] virtual G_completed g_impl() { std::cout << "A::g()" << std::endl; return {42}; } public: [[nodiscard]] int g() { return g_impl().value(); } }; class B : public A { G_completed g_impl() override { auto r = A::g_impl(); std::cout << "B::g()" << std::endl; return r; } public: }; class C : public A { G_completed g_impl() override { std::cout << "C::g()" << std::endl; auto r = A::g_impl(); return r.replace_by(r.value() + 1); } public: }; |
В качестве дисклаймера: лично я считаю, что именно такая постановка задачи выглядит так себе. Поскольку ценность переопределения виртуальных методов в наследниках именно в том, что мы можем делать что хотим, в том числе и не вызывать реализацию из базового класса. И терять эту ценность на ровном месте... Ну такое себе.
Как по мне, так от случая "в наследнике забыли вызывать реализацию из базового класса" нужно защищаться другими средствами. В первую очередь тестами. Но никак не упражнениями в том, насколько хитрый C++ный код можно написать, чтобы компилятор ударил по рукам нерадивого программиста.
Однако, сама по себе логическая задачка мне показалась забавной и интересной, поэтому с удовольствием посвятил ей некоторое время, дабы мозги поразмять и отвлечься от наскучившей рутины.
PS. Еще мне кажется, что обилие совершенно разных решений, предложенных в RSDN-овском обсуждении, указывает на то, что C++ всегда был, есть и будет языком велосипедостроителей для велосипедостроителей. И, пока эти самые велосипедостроители не перевелись, и C++ будет востребованным. Местами ;)
Комментариев нет:
Отправить комментарий