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 не является заменой полиморфизма, но нас интересуют случаи, когда оба шаблона могут использоваться. (Ради этого вопроса нас не интересуют плюсы и минусы каждого шаблона.)
-
Этот question был задан раньше, но оказалось, что автор хотел реализовать именованный параметр idiom и свой собственный answer сосредоточиться на этой проблеме больше, чем на CRTP. С другой стороны, наиболее голосуемый ответ, похоже, касается метода производного класса, вызывающего его омоним в базовом классе.
-
Я придумал ответ (размещен ниже), который содержит довольно много кода шаблона, и мне интересно, есть ли более простые альтернативы.
Ответы
Ответ 1
(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 включает специальные функции-члены public, когда используется = default;
. В этом случае следует использовать
protected:
~A() {}
Тем не менее, защита деструктора недостаточна для случаев, когда вызов конструктора не соответствует вызову деструктора (это может произойти с помощью operator new
). Следовательно, рекомендуется также защищать все конструкторы (включая copy-and-move-constructor).
Если экземпляры 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?
Ответ 2
Примечание. Это не является решением проблемы "окончательного переопределения", а для проблемы многоуровневого наследования CRTP в целом (поскольку я не нашел ответа нигде о том, как это сделать, и я думаю мои выводы были бы полезны).
РЕДАКТИРОВАТЬ: я опубликовал решение проблемы с окончательной отменой здесь
Недавно я узнал о CRTP и его потенциале в качестве статической замены полиморфизма во время выполнения. После некоторого времени поиска, чтобы увидеть, можно ли использовать CRTP как замену для "полифоризма", подобную "за кадром", чтобы вы могли использовать многоуровневое наследование и тому подобное, я должен сказать, что я был довольно удивлен что я не мог найти правильное общее решение где угодно без шаблона, который мог бы масштабироваться бесконечно. В конце концов, почему бы не попытаться сделать CRTP заменой для полиморфизма, учитывая все его преимущества в производительности? Некоторое расследование последовало, и вот что я придумал:
Проблема:
Классический шаблон CRTP создает "цикл" доступности между интерфейсом CRTP и классом реализации. (Класс интерфейса CRTP имеет доступ к "базовому" классу реализации посредством статического приведения его к типу параметра шаблона, а класс реализации наследует открытый интерфейс из класса интерфейса CRTP.) Когда вы создаете конкретную реализацию, вы закрывая цикл, что очень сложно унаследовать от конкретного класса реализации, так что все, происходящее от него, также ведет себя полиморфно.
Классическое одноуровневое наследование CRTP
Решение:
Разделите шаблон на три понятия:
- "абстрактный класс интерфейса", т.е. интерфейс CRTP.
- "Наследуемый класс реализации", который может быть бесконечно унаследован от других наследуемых классов реализации.
- "конкретный класс", который объединяет абстрактный интерфейс с желаемым наследуемым классом реализации и закрывает цикл.
Здесь приведена диаграмма, иллюстрирующая:
Многоуровневое наследование с помощью CRTP
Хитрость заключается в том, чтобы передать конкретный класс реализации в качестве параметра шаблона, вплоть до всех наследуемых классов реализации, в абстрактный класс интерфейса.
При таком подходе вы можете:
- наследовать реализации неопределенно долго,
- вызывать самый высокий реализованный метод в многоуровневой цепочке наследования CRTP с любого уровня,
- дизайн каждой реализации в агностической иерархии,
- забыть использовать шаблонный код (ну, не более, чем с классическим одноуровневым 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, возможно, я опубликую их позже), которые действовали как указатель на виртуальный абстрактный базовый класс, я смог эффективно создавать вектор классов CRTP, наследующих от одного и того же интерфейса.
Я измерил производительность по сравнению с вектором подобных виртуальных классов, все на основе эквивалентного виртуального интерфейса, и я обнаружил, что при таком подходе, в зависимости от сценария, я мог бы достичь увеличения производительности до 8x! Это очень обнадеживает, учитывая относительно небольшие накладные расходы, необходимые для создания функционально полиморфной иерархии классов CRTP!
Ответ 3
Поняв, что мой оригинальный ответ на самом деле не имел дело с окончательным вопросом о переопределении, я подумал, что добавлю к нему. Я хотел придумать решение "окончательного переопределения" таким же образом, как и мой предыдущий ответ.
Проблема:
Класс интерфейса CRTP всегда перенаправляет через статический приведение к самому высокому производному классу. Это противоречит понятию "конечной" функции: если желаемая "конечная" функция не реализована в самом высоком производном классе и "переопределена" более высоким классом (поскольку вы не можете дать функции "окончательный" ) свойство, если оно не является виртуальным, которого мы пытаемся избежать в CRTP), интерфейс CRTP перенаправляет не на желаемую "конечную" функцию, а на "переопределение".
Решение:
Разделите интерфейс на три понятия:
- Абстрактный класс интерфейса без каких-либо функций перенаправления, который наследует:
- абстрактный класс перенаправления, перенаправляющие функции которого перенаправляются на старший производный класс, , если одна или несколько функций перенаправления переопределены:
- конкретный класс переопределения перенаправления, который переопределяет функции перенаправления с помощью реализации.
При создании экземпляра конкретного класса реализации вместо передачи конкретного класса реализации в качестве параметра шаблона через все "наследуемые классы реализации" в интерфейс мы передаем класс перенаправления, который интерфейс наследует как параметр шаблона.
Когда мы хотим сделать функцию "final", мы просто создаем "класс переопределения перенаправления", который наследуется от абстрактного класса перенаправления и переопределяет функцию перенаправления, которую мы хотим сделать final. Затем мы передаем этот новый класс переопределения перенаправления как параметр через все наследуемые классы реализации.
При таком подходе:
- "окончательные" функции вызываются непосредственно, а не перенаправляются посредством приведения (если вам не нужна "конечная" функция, которая должна быть реализована в наследуемом классе реализации, а не в классе переопределения перенаправления),
- "окончательные" функции не могут быть переопределены каким-либо будущим кодом пользователя,
- каждая "конечная" функция требует только дополнительного класса 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()
Ответ 4
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, будет переопределен.
Ответ 5
Многоуровневое наследование не является проблемой, но как 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); }
};
Ответ 6
Вот возможная реализация, которая может уменьшить код шаблона внутри класса (но не общий объем вспомогательного кода).
Идея этого решения заключается в использовании SFINAE и перегрузке для выбора функции impl.
(i) Класс A
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) Класс C
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 является последним вариантом, когда компилятор пытается определить наилучшее соответствие между функциями перегрузки).
Ответ 7
В этой теме происходит много вещей, которые я не нахожу полезными, поэтому я поделюсь своим собственным решением этой проблемы.
CRTP - это в основном шаблон для сокращения кода. Для правильной работы необходимо, чтобы на каждом уровне иерархии наследования можно было вызывать все функции с уровнями ниже - как в обычном динамическом наследовании.
Однако в CRTP каждая стадия должна дополнительно знать о конечном типе, который в настоящее время происходит от него, потому что в конце концов это весь смысл CRTP - вызывать функции, которые всегда применяются к текущему (статическому) конечному типу.
Это можно получить, добавив слой косвенности на каждом уровне статической иерархии наследования, как в следующем примере:
template<typename derived_t>
struct level0_impl
{
auto const& derived() const
{
return static_cast<derived_t const&>(*this);
}
};
struct level0 : public level0_impl<level0>
{
using level0_impl<level0>::level0_impl;
};
template<typename derived_t>
struct level1_impl : level0_impl<derived_t>
{
auto only_for_level1_and_derived() const
{
return derived().foo;
};
auto do_something() const { std::cout<<"hi"<<std::endl; }
};
struct level1 : public level1_impl<level1>
{
using level1_impl<level1>::level1_impl;
};
template<typename derived_t>
struct level2_impl : public level1_impl<derived_t>
{
auto only_for_level2_and_derived() const
{
return derived().bar;
};
};
struct level2 : public level2_impl<level2>
{
using level2_impl<level2>::level2_impl;
};
// ... and so on ...
Можно использовать это с окончательным типом как в следующем:
#include <iostream>
struct final : public level2_impl<final>
{
int foo = 1;
double bar = 2.0;
};
int main()
{
final f{};
std::cout<< f.only_for_level1_and_derived() <<std::endl; //prints variable foo = 1
std::cout<< f.only_for_level2_and_derived() <<std::endl; //prints variable bar = 2.0
}
Кроме того, каждый уровень можно использовать для отдельного уровня, просто _impl
суффикс _impl
:
level1{}.do_something(); //prints "hi"
Это хорошая вещь, которая особенно не работает с другими подходами в этой теме, такими как
template<typename T> class A { auto& derived() {return static_cast<T&>(*this);} };
template<typename T> class B : A<B<T> > {};
template<typename T> class C : B<C> {}; //here derived() in the base class does
//not return C, but B<C> -- which is
//not what one usually wants in CRTP