и с Рождеством всех!
Я изучаю статический полиморфизм и читаю прекрасную книгу Андрея Александреску о политическом дизайне. В моем коде я обнаружил следующее: у меня есть интерфейс Interface
который определяет этот метод Foo
должен присутствовать. Этот интерфейс будет реализован классом Impl
, У меня есть следующие два варианта:
1) Динамический полиморфизм
class Interface {
public:
virtual void Foo() = 0;
}
class Impl : public Interface {
public:
void Foo() {};
}
2) Статический полиморфизм
class Impl {
{
public:
void Foo() {};
}
template <class I>
class Interface : public I
{
public:
void Foo() { I::Foo(); } //not actually needed
}
Имеет ли смысл в этом случае использовать статический полиморфизм? Второй подход дает какие-либо преимущества по сравнению с первым? Интерфейс определяет только наличие некоторых методов, и его механика одинакова для разных реализаций — поэтому не совсем так, как в случаях, описанных в книге, поэтому я чувствую, что могу только усложнять вещи.
Обновить: Мне не нужно полиморфное поведение во время выполнения; правильная реализация известна во время компиляции.
Динамический полиморфизм заставляет ребенка уважать интерфейс.
Статический полиморфизм НЕ заставляет ребенка уважать интерфейс
(пока вы действительно не вызовете функцию), поэтому, если вы не предоставите полезный метод,
вы можете использовать напрямую Impl
,
class InvalidImpl {}; // Doesn't respect interface.
void bar()
{
InvalidImpl invalid;
// this compiles, as not "expected" since InvalidImpl doesn't respect Interface.
CRTP_Interface<InvalidImpl> crtp_invalid;
#if 0 // Any lines of following compile as expected.
invalid.Foo();
crtp_invalid.Foo();
#endif
}
У вас есть 3-й способ использования признаков для проверки того, что класс проверяет интерфейс:
#include <cstdint>
#include <type_traits>
// Helper macro to create traits class to know if class has a member method
#define HAS_MEM_FUNC(name, Prototype, func) \
template<typename U> \
struct name { \
typedef std::uint8_t yes; \
typedef std::uint16_t no; \
template <typename T, T> struct type_check; \
template <typename T = U> \
static yes &chk(type_check<Prototype, &T::func> *); \
template <typename > static no &chk(...); \
static constexpr bool value = sizeof(chk<U>(0)) == sizeof(yes); \
}
// Create traits has_Foo.
HAS_MEM_FUNC(has_Foo, void (T::*)(), Foo);
// Aggregate all requirements for Interface
template <typename T>
struct check_Interface :
std::integral_constant<bool, has_Foo<T>::value /* && has_otherMethod<T>::value */>
{};
// Helper macros to assert if class does respect interface or not.
#define CHECK_INTERFACE(T) static_assert(check_Interface<T>::value, #T " doesn't respect the interface")
#define CHECK_NOT_INTERFACE(T) static_assert(!check_Interface<T>::value, #T " does respect the interface")
Давайте проверим это:
class Interface {
public:
virtual void Foo() = 0;
};
class Child_Impl final : public Interface {
public:
void Foo() override {};
};
#if 0 // Following doesn't compile as expected.
class Child_InvalidImpl final : public Interface {};
#endif
template <class I>
class CRTP_Interface : public I
{
public:
void Foo() { I::Foo(); } // not actually needed
};
class Impl { public: void Foo(); }; // Do respect interface.
class InvalidImpl {}; // Doesn't respect interface.
CHECK_INTERFACE(Interface);
CHECK_INTERFACE(Child_Impl);
CHECK_INTERFACE(Impl);
CHECK_INTERFACE(CRTP_Interface<Impl>);
CHECK_NOT_INTERFACE(InvalidImpl);
CHECK_INTERFACE(CRTP_Interface<InvalidImpl>); // CRTP_Interface<T> _HAS_ Foo (which cannot be invoked)
С динамическим полиморфизмом вы можете оплатить виртуальный звонок. Вы можете уменьшить виртуальный вызов, добавив final
как class Child final : public Interface
,
Таким образом, компилятор может оптимизировать код следующим образом:
void bar(Child& child) { child.Foo(); } // may call Child::Foo not virtually.
но он не может творить волшебство (при условии bar
не указывается) с:
void bar(Interface& child) { child.Foo(); } // have to virtual call Foo.
Теперь предположим, что в вашем интерфейсе есть:
void Interface::Bar() { /* some code */ Foo(); }
мы находимся во втором случае, когда мы должны виртуальный звонок Foo
,
Статический полиморфизм решает это путем:
template<class Derived>
void Interface<Derived>::Bar() { /* some code */ static_cast<Derived*>(this)->Foo(); }
Имеет ли смысл использовать статический полиморфизм, зависит от того, как вы используете класс.
Виртуальные функции вводят уровень косвенности. Виртуальные функции позволяют вызывать метод в производном классе, используя указатель или ссылку на объект базового класса (который является общим для всех производных классов).
Статический полиморфизм не использует общий базовый класс. Каждый производный класс использует свой собственный базовый класс. Эти базовые классы часто создаются из общего шаблона класса. Тем не менее, они разные классы. Это приводит к таким вещам, которые, например, указатели или ссылки на такие объекты не могут храниться в общем контейнере.