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 не является заменой полиморфизма, но нас интересуют случаи, когда можно использовать оба шаблона. (Ради этого вопроса нас не интересуют плюсы и минусы каждого шаблона.)
Этот вопрос задавался ранее, но оказалось, что автор хотел реализовать именованный параметр идиома, и его собственный ответ больше фокусируется на этой проблеме, чем на CRTP. С другой стороны, ответ с наибольшим количеством голосов, кажется, как раз о методе производного класса, вызывающего его омоним в базовом классе.
Я пришел с ответом (опубликованным ниже), в котором достаточно много стандартного кода, и мне интересно, есть ли более простые альтернативы.
8 ответов
(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 в целом (поскольку я нигде не нашел ответа о том, как это сделать, и я думаю, что мои выводы помогут быть полезным).
РЕДАКТИРОВАТЬ: я опубликовал решение последней проблемы переопределения здесь
Недавно я узнал о 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 variadic, возможно, я опубликую это позже), которая действовала как указатель на виртуальный абстрактный базовый класс, я смог эффективно создать вектор классов CRTP, наследуемых от одного и того же интерфейса.
Я измерил производительность по сравнению с вектором виртуальных классов, похожих на аналогичные, основанных на эквивалентном виртуальном интерфейсе, и обнаружил, что при таком подходе, в зависимости от сценария, я могу добиться увеличения производительности до 8 раз! Это очень обнадеживает, учитывая относительно небольшие накладные расходы, необходимые для создания функционально полиморфной иерархии классов CRTP!
Понимая, что мой первоначальный ответ на самом деле не касался последнего имеющегося вопроса о переопределении, я решил добавить к нему. Я хотел придумать решение "окончательного переопределения" так же, как и мой предыдущий ответ.
Эта проблема:
Интерфейсный класс CRTP всегда перенаправляет через статическое приведение к наивысшему производному классу. Это противоречит концепции "конечной" функции: если требуемая "конечная" функция не реализована в самом высоком производном классе и "переопределяется" вышестоящим классом (поскольку вы не можете дать функции "конечную") Если это свойство не является виртуальным, которого мы пытаемся избежать в CRTP), интерфейс CRTP будет перенаправлять не на желаемую "конечную" функцию, а на "переопределение".
Решение:
Разделите интерфейс на три концепции:
- Абстрактный интерфейсный класс без каких-либо перенаправляющих функций, который наследует:
- абстрактный класс перенаправления, чьи функции перенаправления перенаправляют на самый высокий производный класс, если только одна или несколько функций перенаправления не переопределены:
- конкретный класс "переадресация перенаправления", который переопределяет функции перенаправления с помощью реализации.
При создании экземпляра конкретного класса реализации вместо передачи конкретного класса реализации в качестве параметра шаблона через все "наследуемые классы реализации" в интерфейс мы передаем класс перенаправления, от которого интерфейс будет наследоваться, в качестве параметра шаблона.
Когда мы хотим сделать функцию "финальной", мы просто создаем "класс переопределения перенаправления", который наследует от абстрактного класса перенаправления и переопределяет функцию перенаправления, которую мы хотим сделать финальной. Затем мы передаем этот новый "класс переопределения перенаправления" в качестве параметра через все наследуемые классы реализации.
При таком подходе:
- "конечные" функции вызываются напрямую, а не перенаправляются через приведение (если только вам не требуется, чтобы "конечная" функция была реализована в наследуемом классе реализации, а не в классе переопределения перенаправления),
- "конечные" функции не могут быть переопределены каким-либо будущим кодом пользователя,
- каждая "конечная" функция требует только дополнительного класса 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()
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, будет переопределен.
В C++23 добавлена функция вывода , которая полностью меняет правила игры, когда речь идет о многоуровневом наследовании CRTP.
С этой функцией многоуровневая иерархия CRTP выглядит так же просто, как:
struct level0
{
auto foo(this auto self) const { return self; };
};
struct level1 : level0 {};
struct level2 : level1 {};
Сравните все остальные ответы здесь, в этой теме, включая мой . Мало того, что некоторые из них не функциональны, они намного, намного сложнее, чем вышеперечисленные.
В качестве еще одного бонуса вывод, что это даже более последовательно, чем предыдущая версия. Например, сравните его со стандартным многоуровневым CRTP,
template<typename derived_t>
struct level0_impl
{
auto foo() const { return static_cast<derived_t const&>(*this); };
};
struct level0 : public level0_impl<level0> { /* using level0::level0 and so on...*/};
template<typename derived_t> struct level1_impl : level0_impl<derived_t> {};
struct level1 : public level1_impl<level1> {};
template<typename derived_t> struct level2_impl : level1_impl<derived_t> {};
struct level2 : public level2_impl<level2> {};
Это не совсем глупо — у меня есть конкретная иерархия вдоль
Однако, если я вызову этот фрагмент,
level2 l2;
auto f = [](level1 const& l1) { return l1; };
auto var = f(l2);
что это даст? Да, ошибка времени компиляции, потому что и не связаны. происходит от
Хорошо, мы умны, поэтому давайте сначала изменим параметр функции на
level2 l2;
auto f = []<typename T>(level1_impl<T> const& l1) { return l1; };
auto var = f(l2);
Какой теперь тип возвращаемого значения? Должно быть что-то вокруг, или нет? Нет, это . То есть, потому что
В частности, если вы определили функции на нескольких уровнях, а возвращаемые типы должны соответствовать друг другу - это боль с "классическим" многоуровневым CRTP. В конечном итоге вы либо всегда работаете с окончательным типом, либо с дублированием каждой функции на любом уровне иерархии — именно этого CRTP призван избежать.
Введите вывод : теперь все работает хорошо и так, как ожидалось. и
struct level0
{
auto foo(this auto self) const { return self; };
};
struct level1 : level0 {};
struct level2 : level1 {};
level2 l2;
auto f = [](level1 const& l1) { return l1; };
auto var = f(l2);
static_assert(std::is_same_v<decltype(var), level1>);
Я упоминал, что этот вывод полностью меняет правила игры для многоуровневого наследования CRTP? По сути, это впервые делает этот шаблон практически доступным , потому что предыдущие подходы неудобны в сопровождении и слишком сложны. Спасибо @barry и tartanllama, а также коллегам за ваше предложение , это действительно отличное расширение языка!
В этой теме происходит много вещей, которые я не нахожу полезными, поэтому я делюсь здесь своим собственным решением этой проблемы.
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
суффикс:
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
Многоуровневое наследование не является проблемой, но как 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); }
};
Вот возможная реализация, которая может уменьшить стандартный код внутри класса (но не общий объем вспомогательного кода).
Идея этого решения заключается в использовании 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 - последний вариант, когда компилятор пытается выяснить лучшее соответствие между функциями перегрузки).