CRTP和多级继承

Cas*_*eri 23 c++ inheritance crtp static-polymorphism c++11

我的一个朋友问我"如何使用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 */ }
}
Run Code Online (Sandbox Code Playgroud)

我和我的朋友都知道CRTP不是多态的替代品,但我们对可以使用这两种模式的情况感兴趣.(为了这个问题,我们对每种模式的利弊都不感兴趣.)

  1. 之前已经问过这个问题,但事实证明作者想要实现命名参数idiom而他自己的答案更多地关注这个问题而不是CRTP.另一方面,投票最多的答案似乎只是一个派生类方法在基类中调用它的同音词.

  2. 我想出了一个答案(下面发布),它有很多样板代码,我想知道是否有更简单的替代品.

Cas*_*eri 12

(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.

};
Run Code Online (Sandbox Code Playgroud)

A<T>::foo()行为类似于纯虚方法,因为它没有" 默认实现 ",并且调用被定向到派生类.但是,这并不妨碍A<T>将其实例化为非基类.要获得这种行为A<T>::~A()作出protected.

备注:不幸的是,GCC错误会在= default;使用时将特殊成员函数公开.在这种情况下,应该使用一个

protected:
    ~A() {}
Run Code Online (Sandbox Code Playgroud)

尽管如此,保护析构函数还不足以满足对构造函数的调用与析构函数调用不匹配的情况(这可能会发生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();
  }

};
Run Code Online (Sandbox Code Playgroud)

构造函数和析构函数是公共的,T默认为void.这允许类型的对象B<>在层次结构中派生得最多,并使其合法:

B<> b;
b.foo();
Run Code Online (Sandbox Code Playgroud)

另请注意,B<T>::foo()在某种意义上,如果B<T>是最派生的类(或者更确切地说,如果Tvoid),则表现为非纯虚方法,然后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";
  }

};
Run Code Online (Sandbox Code Playgroud)

或者,C::foo()如果继承的implementation(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();
}
Run Code Online (Sandbox Code Playgroud)

哪个印刷品:

### 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()
Run Code Online (Sandbox Code Playgroud)

使用这种方法,以及"变体样式"包装器(使用一些sechsy可变参数模板和宏构建,也许我稍后会发布),它就像一个指向虚拟抽象基类的指针,我能够有效地创建一个从同一接口继承的CRTP类的向量.

我测量了性能与类似的类虚拟类的向量相比较,所有这些都基于等效的虚拟接口,我发现使用这种方法,根据场景,我可以实现高达8倍的性能提升!考虑到生成功能多态CRTP类层次结构所需的开销相对较少,这非常令人鼓舞!


小智 6

在意识到我原来的答案实际上并没有解决手头的最终覆盖问题后,我想我应该添加它。我想以与我之前的答案类似的方式提出“最终覆盖”解决方案。

问题:

CRTP 接口类始终通过静态转换重定向到最高的派生类。这与“最终”函数的概念不一致:如果所需的“最终”函数未在最高派生类上实现,并且被更高的类“覆盖”(因为您不能为函数赋予“最终”)属性,除非它是虚拟的(我们在 CRTP 中试图避免这种情况),CRTP 接口将不会重定向到所需的“最终”函数,而是重定向到“覆盖”。

解决方案:

将界面分为三个概念:

  • 没有任何重定向功能的抽象接口类,它继承:
  • 一种抽象重定向类,其重定向函数重定向到最高的派生类,除非一个或多个重定向函数被以下项重写:
  • 一个具体的“重定向覆盖”类,它用一个实现覆盖重定向函数。

当实例化具体实现类时,我们不是将具体实现类作为模板参数通过所有“可继承实现类”传递到接口中,而是将接口将从其继承的重定向类作为模板参数传递。

当我们想让一个函数成为“final”时,我们只需创建一个“重定向覆盖类”,它继承自抽象重定向类,并覆盖我们想要成为final的重定向函数。然后我们将这个新的“重定向覆盖类”作为参数传递给所有可继承的实现类。

通过这种方法:

  1. “final”函数是直接调用的,而不是通过强制转换重定向(除非您需要在可继承的实现类而不是重定向覆盖类中实现“final”函数),
  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();
}
Run Code Online (Sandbox Code Playgroud)

代码打印:

### 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()
Run Code Online (Sandbox Code Playgroud)