CRTP и многоуровневое наследование

Мой друг спросил меня «Как использовать CRTP для замены полиморфизма в многоуровневом наследовании». Точнее, в такой ситуации:

struct A {

void bar() {
// do something and then call foo (possibly) in the derived class:
foo();
}

// possibly non pure virtual
virtual void foo() const = 0;
}

struct B : A {
void foo() const override { /* do something */ }
}

struct C : B {
// possibly absent to not override B::foo().
void foo() const final { /* do something else */ }
}

Мы с моим другом понимаем, что CRTP не является заменой полиморфизма, но нас интересуют случаи, когда можно использовать оба шаблона. (Ради этого вопроса нас не интересуют плюсы и минусы каждого шаблона.)

  1. это вопрос был задан ранее, но оказалось, что автор хотел реализовать именованный параметр идиома и его собственный ответ сосредоточиться на этой проблеме больше, чем на CRTP. С другой стороны, наиболее проголосовали ответ Похоже, это просто метод производного класса, вызывающий его омоним в базовом классе.

  2. Я пришел с ответом (опубликованным ниже), который содержит довольно много стандартного кода, и мне интересно, есть ли более простые альтернативы.

21

Решение

(1) Самый верхний класс в иерархии выглядит так:

template <typename T>
class A {

public:

void bar() const {
// do something and then call foo (possibly) in the derived class:
foo();
}

void foo() const {
static_cast<const T*>(this)->foo();
}

protected:

~A() = default;

// Constructors should be protected as well.

};

A<T>::foo() ведет себя подобно чисто виртуальному методу в том смысле, что он не имеет «реализация по умолчанию«и вызовы направлены на производные классы. Однако это не мешает A<T> от создания экземпляра как не базовый класс. Чтобы получить это поведение A<T>::~A() сделан protected,

Замечание: к сожалению Ошибка GCC делает специальные функции-члены публичными, когда = default; используется. В этом случае следует использовать

protected:
~A() {}

Тем не менее, защита деструктора недостаточна для случаев, когда вызов конструктора не совпадает с вызовом деструктора (это может произойти через operator new). Следовательно, желательно также защитить все конструкторы (включая конструкторы копирования и перемещения).

Когда экземпляры A<T> должно быть разрешено и A<T>::foo() должен вести себя как не чистый виртуальный метод, то A должен быть похож на шаблон класса B ниже.

(2) Классы в середине иерархии (или самой верхней, как описано в параграфе выше) выглядят так:

template <typename T = void>
class B : public A<B<T>> { // no inherinace if this is the topmost class

public:

// Constructors and destructor

// boilerplate code :-(
void foo() const {
foo_impl(std::is_same<T, void>{});
}

private:

void foo_impl(std::true_type) const {
std::cout << "B::foo()\n";
}

// boilerplate code :-(
void foo_impl(std::false_type) const {
if (&B::foo == &T::foo)
foo_impl(std::true_type{});
else
static_cast<const T*>(this)->foo();
}

};

Конструкторы и деструкторы являются публичными и T по умолчанию void, Это позволяет объекты типа B<> чтобы быть наиболее производным в иерархии и делает это законным:

B<> b;
b.foo();

Обратите внимание также, что B<T>::foo() ведет себя как не чистый виртуальный метод в том смысле, что если B<T> является наиболее производным классом (или, точнее, если T является void), затем b.foo(); называет «реализация по умолчанию foo()«(какие выводы B::foo()). Если T не является voidзатем вызов направляется в производный класс. Это достигается с помощью диспетчеризации тегов.

Тест &B::foo == &T::foo требуется, чтобы избежать бесконечного рекурсивного вызова. Действительно, если производный класс, Tне переопределение foo(), вызов static_cast<const T*>(this)->foo(); будет разрешать B::foo() какие звонки B::foo_impl(std::false_type) снова. Кроме того, этот тест может быть решен во время компиляции, и код if (true) или же if (false) и оптимизатор может полностью удалить тест (например, GCC с -O3).

(3) Наконец, нижняя часть иерархии выглядит так:

class C : public B<C> {

public:

void foo() const {
std::cout << "C::foo()\n";
}

};

В качестве альтернативы можно удалить C::foo() полностью если унаследованная реализация (B<C>::foo()) является адекватным.

Заметить, что C::foo() аналогичен конечному методу в том смысле, что его вызов не перенаправляет вызов производному классу (если есть). (Чтобы сделать его не окончательным, класс шаблона B должен быть использован.)

(4) Смотрите также:

Как избежать ошибок при использовании CRTP?

9

Другие решения

Примечание. Это не является конкретно решением проблемы «окончательного переопределения», но является проблемой многоуровневого наследования CRTP в целом (поскольку я нигде не нашел ответа о том, как это сделать, и я думаю, что мои выводы помогут быть полезным).

РЕДАКТИРОВАТЬ: Я опубликовал решение последней проблемы переопределения Вот

Недавно я узнал о CRTP и его потенциале как статической замене полиморфизма во время выполнения. Пройдя некоторое время, чтобы понять, можно ли использовать CRTP в качестве аналога «вставной» замены для полиморфизма, чтобы вы могли использовать многоуровневое наследование и тому подобное, я должен сказать, что был довольно удивлен что я не мог найти правильного общего решения нигде без шаблона, который мог бы масштабироваться до бесконечности. В конце концов, почему бы не попробовать сделать CRTP заменой полиморфизма, учитывая все его преимущества в производительности? Некоторое расследование последовало, и вот что я придумал:

Эта проблема:

Классический шаблон CRTP создает «петлю» доступности между интерфейсом CRTP и классом реализации. (Класс интерфейса CRTP имеет доступ к «базовому» классу реализации посредством статического приведения себя к типу параметра шаблона, а класс реализации наследует открытый интерфейс от класса интерфейса CRTP.) Когда вы создаете конкретную реализацию, вы закрытие цикла, что делает очень трудным наследование от конкретного класса реализации, так что все, что происходит от него, также ведет себя полиморфно.

Классическое одноуровневое наследование CRTP

Решение:

Разделите шаблон на три понятия:

  • «Абстрактный класс интерфейса», то есть интерфейс CRTP.
  • «Унаследованный класс реализации», который может неограниченно наследоваться другими наследуемыми классами реализации.
  • «Конкретный класс», который объединяет абстрактный интерфейс с желаемым наследуемым классом реализации и закрывает цикл.

Вот диаграмма, чтобы помочь проиллюстрировать:

Многоуровневое наследование с CRTP

Хитрость заключается в том, чтобы передать конкретный класс реализации в качестве параметра шаблона полностью через все наследуемые классы реализации в класс абстрактного интерфейса.

При таком подходе вы можете:

  1. наследовать реализации бесконечно,
  2. вызывать самый высокий реализованный метод в цепочке многоуровневого наследования CRTP из любого уровня,
  3. разрабатывать каждую реализацию в независимой от иерархии манере,
  4. забудьте о необходимости использования стандартного кода (ну, в любом случае, не более чем с классическим одноуровневым CRTP),

который прекрасно отражает виртуальный / динамический полиморфизм.

Пример кода:

#include <iostream>

template <class Top>
struct CrtpInterface
{
void foo()
{
std::cout << "Calling CrtpInterface::foo()\n";
fooImpl();
}
void foo2()
{
std::cout << "Calling CrtpInterface::foo2()\n";
fooImpl2();
}
void foo3()
{
std::cout << "Calling CrtpInterface::foo3()\n";
fooImpl3();
}
void foo4()
{
std::cout << "Calling CrtpInterface::foo4()\n";
fooImpl4();
}

// The "pure virtual functions"protected:
inline void fooImpl()
{
top().fooImpl();
}
inline void fooImpl2()
{
top().fooImpl2();
}
inline void fooImpl3()
{
top().fooImpl3();
}
inline void fooImpl4()
{
top().fooImpl4();
}
inline Top& top()
{
return static_cast<Top&>(*this);
}
};

template<class Top>
class DefaultImpl : public CrtpInterface<Top>
{
using impl = CrtpInterface<Top>;
friend impl;

void fooImpl()
{
std::cout << "Default::fooImpl()\n";
}

void fooImpl2()
{
std::cout << "Default::fooImpl2()\n";
std::cout << "Calling foo() from interface\n";
impl::foo();
}

void fooImpl3()
{
std::cout << "Default::fooImpl3()\n";
std::cout << "Calling highest level fooImpl2() from interface\n";
impl::fooImpl2();
}

void fooImpl4()
{
std::cout << "Default::fooImpl4()\n";
std::cout << "Calling highest level fooImpl3() from interface\n";
impl::fooImpl3();
}
};

template<class Top>
class AImpl : public DefaultImpl<Top>
{
using impl = CrtpInterface<Top>;
friend impl;

void fooImpl()
{
std::cout << "A::fooImpl()\n";
}
};

struct A : AImpl<A>
{
};

template<class Top>
class BImpl : public AImpl<Top>
{
using impl = CrtpInterface<Top>;
friend impl;

protected:
BImpl()
: i{1}
{
}

private:
int i;
void fooImpl2()
{
std::cout << "B::fooImpl2(): " << i << "\n";
}
};

struct B : BImpl<B>
{
};

template<class Top>
class CImpl : public BImpl<Top>
{
using impl = CrtpInterface<Top>;
friend impl;

protected:
CImpl(int x = 2)
: i{x}
{
}

private:
int i;
void fooImpl3()
{
std::cout << "C::fooImpl3(): " << i << "\n";
}
};

struct C : CImpl<C>
{
C(int i = 9)
: CImpl(i)
{
}
};

template<class Top>
class DImpl : public CImpl<Top>
{
using impl = CrtpInterface<Top>;
friend impl;

void fooImpl4()
{
std::cout << "D::fooImpl4()\n";
}
};

struct D : DImpl<D>
{
};

int main()
{
std::cout << "### A ###\n";
A a;
a.foo();
a.foo2();
a.foo3();
a.foo4();

std::cout << "### B ###\n";
B b;
b.foo();
b.foo2();
b.foo3();
b.foo4();

std::cout << "### C ###\n";
C c;
c.foo();
c.foo2();
c.foo3();
c.foo4();

std::cout << "### D ###\n";
D d;
d.foo();
d.foo2();
d.foo3();
d.foo4();
}

Какие отпечатки:

### A ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
Default::fooImpl2()
Calling foo() from interface
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo3()
Default::fooImpl3()
Calling highest level fooImpl2() from interface
Default::fooImpl2()
Calling foo() from interface
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo4()
Default::fooImpl4()
Calling highest level fooImpl3() from interface
Default::fooImpl3()
Calling highest level fooImpl2() from interface
Default::fooImpl2()
Calling foo() from interface
Calling CrtpInterface::foo()
A::fooImpl()
### B ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
Default::fooImpl3()
Calling highest level fooImpl2() from interface
B::fooImpl2(): 1
Calling CrtpInterface::foo4()
Default::fooImpl4()
Calling highest level fooImpl3() from interface
Default::fooImpl3()
Calling highest level fooImpl2() from interface
B::fooImpl2(): 1
### C ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
C::fooImpl3(): 9
Calling CrtpInterface::foo4()
Default::fooImpl4()
Calling highest level fooImpl3() from interface
C::fooImpl3(): 9
### D ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
C::fooImpl3(): 2
Calling CrtpInterface::foo4()
D::fooImpl4()

Используя этот подход и «обертку в стиле варианта» (построенную с использованием некоторых шаблонов и макросов sechsy variadic, возможно, я опубликую это позже), которая действовала как указатель на виртуальный абстрактный базовый класс, я смог эффективно создать вектор классов CRTP, наследуемых от одного и того же интерфейса.

Я измерил производительность по сравнению с вектором виртуальных классов, похожих на аналогичные, основанных на эквивалентном виртуальном интерфейсе, и обнаружил, что при таком подходе, в зависимости от сценария, я могу добиться увеличения производительности до 8 раз! Это очень обнадеживает, учитывая относительно небольшие накладные расходы, необходимые для создания функционально полиморфной иерархии классов CRTP!

7

Поняв, что мой оригинальный ответ на самом деле не имел дело с последним вопросом переопределения, я думал, что добавлю к нему. Я хотел придумать решение «окончательного переопределения» так же, как и мой предыдущий ответ.

Эта проблема:

Интерфейсный класс CRTP всегда перенаправляет через статическое приведение к наивысшему производному классу. Это противоречит концепции «конечной» функции: если требуемая «конечная» функция не реализована в самом высоком производном классе и «переопределяется» вышестоящим классом (поскольку вы не можете дать функции «конечную») Если это свойство не является виртуальным, которого мы пытаемся избежать в CRTP), интерфейс CRTP будет перенаправлять не на желаемую «конечную» функцию, а на «переопределение».

Решение:

Разделите интерфейс на три концепции:

  • Абстрактный интерфейсный класс без каких-либо перенаправляющих функций, который наследует:
  • абстрактный класс перенаправления, чьи функции перенаправления перенаправляют в самый высокий производный класс, если одна или несколько функций перенаправления переопределяются:
  • конкретный класс «переадресация перенаправления», который переопределяет функции перенаправления с помощью реализации.

При создании экземпляра конкретного класса реализации вместо передачи конкретного класса реализации в качестве параметра шаблона через все «наследуемые классы реализации» в интерфейс мы передаем класс перенаправления, от которого интерфейс будет наследоваться, в качестве параметра шаблона.

Когда мы хотим сделать функцию «конечной», мы просто создаем «класс переопределения перенаправления», который наследуется от абстрактного класса перенаправления и переопределяет функцию перенаправления, которую мы хотим сделать финальной. Затем мы передаем этот новый «класс переадресации перенаправления» в качестве параметра через все наследуемые классы реализации.

При таком подходе:

  1. «конечные» функции вызываются напрямую, а не перенаправляются через приведение (если только вам не требуется, чтобы «конечная» функция была реализована в наследуемом классе реализации, а не в классе переопределения перенаправления),
  2. «конечные» функции не могут быть переопределены каким-либо будущим кодом пользователя,
  3. каждая «конечная» функция требует только дополнительного класса ImplFinal для каждого уровня наследования, без дополнительного шаблонного шаблона.

Все это звучит очень сложно, поэтому вот схема, которую я сделал, чтобы упростить понимание:

DImpl и EImpl имеют конечные функции, которые не могут быть переопределены, когда DImpl или EImpl наследуются от:

Пример кода:

#include <iostream>
#include <type_traits>

template <class Top>
struct Redirect
{
protected:
// The "pure virtual functions"inline void fooImpl()
{
top().fooImpl();
}
inline void fooImpl2()
{
top().fooImpl2();
}
inline void fooImpl3()
{
top().fooImpl3();
}
inline void fooImpl4()
{
top().fooImpl4();
}
inline Top& top()
{
// GCC doesn't allow static_cast<Top&>(*this)
// since Interface uses private inheritance
static_assert(std::is_base_of<Redirect, Top>::value, "Invalid Top class specified.");
return (Top&)(*this);
}
};

// Wraps R around the inner level of a template T, e.g:
// R := Redirect, T := X, then inject_type::type := Redirect<X>
// R := Redirect, T := A<B<C<X>>>, then inject_type::type := A<B<C<Redirect<X>>>>
template<template<class> class R, class T>
struct inject_type
{
using type = R<T>;
};

template<template<class> class R, class InnerFirst, class... InnerRest, template<class...> class Outer>
struct inject_type<R, Outer<InnerFirst, InnerRest...>>
{
using type = Outer<typename inject_type<R, InnerFirst>::type, InnerRest...>;
};

// We will be inheriting either Redirect<...> or something
// which derives from it (and overrides the functions).
// Use private inheritance, so that all polymorphic calls can
// only go through this class (which makes it impossible to
// subvert redirect overrides using future user code).
template <class V>
struct Interface : private inject_type<Redirect, V>::type
{
using impl = Interface;

void foo()
{
std::cout << "Calling Interface::foo()\n";
fooImpl();
}

void foo2()
{
std::cout << "Calling Interface::foo2()\n";
fooImpl2();
}

void foo3()
{
std::cout << "Calling Interface::foo3()\n";
fooImpl3();
}

void foo4()
{
std::cout << "Calling Interface::foo4()\n";
fooImpl4();
}

private:
using R = typename inject_type<::Redirect, V>::type;

protected:
using R::fooImpl;
using R::fooImpl2;
using R::fooImpl3;
using R::fooImpl4;
};

template<class V>
struct DefaultImpl : Interface<V>
{
template<class>
friend struct Redirect;

protected:
// Picking up typename impl from Interface, where all polymorphic calls must pass through
using impl = typename DefaultImpl::impl;

void fooImpl()
{
std::cout << "Default::fooImpl()\n";
}

void fooImpl2()
{
std::cout << "Default::fooImpl2()\n";
std::cout << "Calling foo() from interface\n";
impl::foo();
}

void fooImpl3()
{
std::cout << "Default::fooImpl3()\n";
std::cout << "Calling highest level fooImpl2() from interface\n";
impl::fooImpl2();
}

void fooImpl4()
{
std::cout << "Default::fooImpl4()\n";
std::cout << "Calling highest level fooImpl3() from interface\n";
impl::fooImpl3();
}
};

template<class V>
struct AImpl : public DefaultImpl<V>
{
template<class>
friend struct Redirect;

protected:
void fooImpl()
{
std::cout << "A::fooImpl()\n";
}
};

struct A : AImpl<A>
{
};

template<class V>
struct BImpl : public AImpl<V>
{
template<class>
friend struct Redirect;

protected:
BImpl()
: i{1}
{
}

private:
int i;
void fooImpl2()
{
std::cout << "B::fooImpl2(): " << i << "\n";
}
};

struct B : BImpl<B>
{
};

template<class V>
struct CImpl : public BImpl<V>
{
template<class>
friend struct Redirect;

protected:
CImpl(int x = 2)
: i{x}
{
}

private:
int i;
void fooImpl3()
{
std::cout << "C::fooImpl3(): " << i << "\n";
}
};

struct C : CImpl<C>
{
C(int i = 9)
: CImpl(i)
{
}
};

// Make D::fooImpl4 final
template<class V>
struct DImplFinal : public V
{
protected:
void fooImpl4()
{
std::cout << "DImplFinal::fooImpl4()\n";
}
};// Wrapping V with DImplFinal overrides the redirecting functions
template<class V>
struct DImpl : CImpl<DImplFinal<V>>
{
};

struct D : DImpl<D>
{
};

template<class V>
struct EImpl : DImpl<V>
{
template<class>
friend struct Redirect;

protected:
void fooImpl()
{
std::cout << "E::fooImpl()\n";
}

void fooImpl3()
{
std::cout << "E::fooImpl3()\n";
}

// This will never be called, because fooImpl4 is final in DImpl
void fooImpl4()
{
std::cout << "E::fooImpl4(): this should never be printed\n";
}
};

struct E : EImpl<E>
{
};

// Make F::fooImpl3 final
template<class V, class Top>
struct FImplFinal : public V
{
protected:
// This is implemented in FImpl, so redirect
void fooImpl3()
{
top().fooImpl3();
}

// This will never be called, because fooImpl4 is final in DImpl
void fooImpl4()
{
std::cout << "FImplFinal::fooImpl4() this should never be printed\n";
}

inline Top& top()
{
// GCC won't do a static_cast directly :(
static_assert(std::is_base_of<FImplFinal, Top>::value, "Invalid Top class specified");
return (Top&)(*this);
}
};

// Wrapping V with FImplFinal overrides the redirecting functions, but only if they haven't been overridden already
template<class V>
struct FImpl : EImpl<FImplFinal<V, FImpl<V>>>
{
template<class>
friend struct Redirect;
template<class, class>
friend struct FImplFinal;

protected:
FImpl()
: i{99}
{
}

// Picking up typename impl from DefaultImpl
using impl = typename FImpl::impl;

private:
int i;

void fooImpl2()
{
std::cout << "F::fooImpl2()\n";
// This will only call DFinal::fooImpl4();
std::cout << "Calling fooImpl4() polymorphically. (Should not print FImplFinal::fooImpl4() or EImpl::fooImpl4())\n";
impl::fooImpl4();
}

void fooImpl3()
{
std::cout << "FImpl::fooImpl3(), i = " << i << '\n';
}
};

struct F : FImpl<F>
{
};

int main()
{
std::cout << "### A ###\n";
A a;
a.foo();
a.foo2();
a.foo3();
a.foo4();

std::cout << "### B ###\n";
B b;
b.foo();
b.foo2();
b.foo3();
b.foo4();

std::cout << "### C ###\n";
C c;
c.foo();
c.foo2();
c.foo3();
c.foo4();

std::cout << "### D ###\n";
D d;
d.foo();
d.foo2();
d.foo3();
d.foo4();

std::cout << "### E ###\n";
E e;
e.foo();
e.foo2();
e.foo3();
e.foo4();

std::cout << "### F ###\n";
F f;
f.foo();
f.foo2();
f.foo3();
f.foo4();
}

Код печатает:

### A ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
Default::fooImpl2()
Calling foo() from interface
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo3()
Default::fooImpl3()
Calling highest level fooImpl2() from interface
Default::fooImpl2()
Calling foo() from interface
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo4()
Default::fooImpl4()
Calling highest level fooImpl3() from interface
Default::fooImpl3()
Calling highest level fooImpl2() from interface
Default::fooImpl2()
Calling foo() from interface
Calling CrtpInterface::foo()
A::fooImpl()
### B ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
Default::fooImpl3()
Calling highest level fooImpl2() from interface
B::fooImpl2(): 1
Calling CrtpInterface::foo4()
Default::fooImpl4()
Calling highest level fooImpl3() from interface
Default::fooImpl3()
Calling highest level fooImpl2() from interface
B::fooImpl2(): 1
### C ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
C::fooImpl3(): 9
Calling CrtpInterface::foo4()
Default::fooImpl4()
Calling highest level fooImpl3() from interface
C::fooImpl3(): 9
### D ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
C::fooImpl3(): 2
Calling CrtpInterface::foo4()
DImplFinal::fooImpl4()
### E ###
Calling CrtpInterface::foo()
E::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
E::fooImpl3()
Calling CrtpInterface::foo4()
DImplFinal::fooImpl4()
### F ###
Calling CrtpInterface::foo()
E::fooImpl()
Calling CrtpInterface::foo2()
F::fooImpl2()
Attempting to call FFinal::fooImpl4() or E::fooImpl4()
DImplFinal::fooImpl4()
Calling CrtpInterface::foo3()
FImpl::fooImpl3(), i = 99
Calling CrtpInterface::foo4()
DImplFinal::fooImpl4()
3

template<class Derived>
struct A {
void foo() {
static_cast<Derived*>(this)->foo();
}
}

template<class Derived>
struct B: A <Derived> {
void foo() {
// do something
}
}

struct C: B <C> {
void foo(); // can be either present or absent
}

если foo () в C отсутствует, будет вызвана функция в B. В противном случае тот, что в B, будет переопределен.

2

Многоуровневое наследование не является проблемой, но как CRTP создает полиморфизм.

template<typename Derived>
struct Base
{
void f() { /* Basic case */ }

// "Pure virtual" method
void pure() { static_cast<Derived*>(this)->pure(); }
};

struct Overriding: Base<Overriding>
{
void f() { /* Special case */ }

// This method must exists to prevent endless recursion in Base::f
void pure() { /* ... */ }
};

struct NonOverriding: Base<NonOverriding>
{
void pure() { /* ... */ }
};

template<typename Derived>
void f(const Base<Derived>& base)
{
base.f();    // Base::f
base.pure(); // Base::pure, which eventually calls Derived::pure

// Derived::f if an overriding method exists.
// Base::f otherwise.
static_cast<const Derived&>(base).f();
}

Можно также ввести derived метод, позволяющий избежать приведения многословных типов при каждом вызове.

template<typename Derived>
struct Base
{
Derived& derived() { return *static_cast<Derived*>(this); }
const Derived& derived() const { return *static_cast<const Derived*>(this); }
};
1

Вот возможная реализация, которая может уменьшить стандартный код внутри класса (но не общий объем вспомогательного кода).

Идея этого решения заключается в использовании SFINAE и перегрузки для выбора функции impl.

(i) Класс А

template <typename T> class A {
void foo() const {
static_cast<const T*>(this)->foo( Type2Type<T> );
}
}

где TypetoType является структурой шаблона

template< typename T > struct Type2Type {
typedef T OriginalType
}

что полезно для помощи компилятору в выборе foo () impl. перегрузкой.

(i) Класс B

template <typename T = void>
class B : public A<B<T>> {

void foo(...) {
std::cout << "B::foo()\n";
}
void foo( Type2Type< std::enable_if< is_base_of<T,B>::value == true, B>::type> ) {
static_cast<const T*>(this)->foo( Type2Type<T> );
}
}

Здесь аргумент TypetoType необязателен, если нижняя часть иерархии задана C.

(ii) Класс С

class C : public B<C> {
void foo(...) {
std::cout << "C::foo()\n";
}
}

Я знаю, что std :: is_base_of возвращает true, если T == B. Здесь мы используем нашу собственную версию is_base_of, которая возвращает false_type, когда два аргумента шаблона совпадают. Что-то вроде

template<class B, class D>
struct is_base_of : public is_convertible<D *, B *> {};
template<class B, class B>
struct is_base_of<B, B> :  public false_type {};
template<class D>
struct is_base_of<void, D> : public false_type {};

Вывод: если std :: enable_if завершится неудачно, то будет доступна только вариативная версия foo () (из-за SFINAE), и компилятор реализует B-версию foo. Однако, если enable_if не дает сбоя, компилятор выберет вторую версию (потому что variadic — последний вариант, когда компилятор пытается выяснить лучшее соответствие между функциями перегрузки).

0
По вопросам рекламы [email protected]