Eigen源码阅读之二:奇异递归模板模式 CRTP


本期重点介绍Eigen贯穿整个Library的设计方法奇异递归模板模式。

一、CRTP基本样式

This oddly named pattern refers to a general class of techniques that consists of passing a derived class as a template argument to one of its own base classes

派生类作为模板参数传给其的一个基类,如下两例所示

template
class CuriousBase {
   …
};

class Curious : public CuriousBase {
   …
};
template
class CuriousBase {
    …
};

template
class CuriousTemplate : public CuriousBase> {
    …
};

二、Eigen中的CRTP

我们首选来看Matrix的定义

template
class Matrix
  : public PlainObjectBase >

PlainObjectBase的定义如下

template
class PlainObjectBase : public internal::dense_xpr_base::type

xprHelper.h中,我们可以看到dense_xpr_base的定义,它的作用也就是用来确定Matrix是个矩阵还是向量。

template
struct dense_xpr_base
{
  typedef MatrixBase type;
};

template
struct dense_xpr_base
{
  typedef ArrayBase type;
};

如果我们以传递的矩阵为例,那么PlainObjectBase就变为如下

template
class PlainObjectBase : public MatrixBase

我们再紧接着MatrixBase往父类上走,会有如下

template struct EigenBase

template
class DenseCoeffsBase : public EigenBase


template class DenseBase
  : public DenseCoeffsBase::value>


template class MatrixBase
  : public DenseBase

更直观的表示,Matrix继承体系如下图所示

graph TD EigenBase -->DenseCoeffsBase DenseCoeffsBase --> DenseBase DenseBase --> MatrixBase MatrixBase --> PlainObjectBase PlainObjectBase

可以看出,在Matrix的继承体系中,直接基类和Matrix使用了CRTP模式,Matrix将自己作为参数传入到继承的基类中。Matrix的基类本身也是继承了基类,如MatrixBase继承了DenseBase,然而MatrixBaseDenseBase并未使用
CRTP模式。

除了Matrix之外,对于一些运算符也使用了CRTP模式,比如CwiseBinaryOp, 其定义如下

template
class CwiseBinaryOp :
  public CwiseBinaryOpImpl<
          BinaryOp, LhsType, RhsType,
          typename internal::cwise_promote_storage_type::StorageKind,
                                                        typename internal::traits::StorageKind,
                                                        BinaryOp>::ret>, internal::no_assignment_operator


template
class CwiseBinaryOpImpl
  : public internal::generic_xpr_base >::type
{
public:
  typedef typename internal::generic_xpr_base >::type Base;
};


template
struct generic_xpr_base
{
  typedef typename dense_xpr_base::type type;
};

其中,dense_xpr_base在上文中有提到,那么总结可以看到最终CwiseBinaryOp还是将自己传给了基类,满足CRTP定义。

三、CRTP使用讨论

3.1 实现原理

基类模板定义了一些接口(或称行为),但是实现却是通过派生类来实现,这样看起来像多态,然而这种依靠不同类型,绑定不同实现的机制是在编译期间就确定了。

我们查看一下EigenBase中的一些接口

//EigenBase中的一些接口

/** \returns a reference to the derived object */
  EIGEN_DEVICE_FUNC
  Derived& derived() { return *static_cast(this); }//获取派生类引用
  /** \returns a const reference to the derived object */
  EIGEN_DEVICE_FUNC
  const Derived& derived() const { return *static_cast(this); }

  EIGEN_DEVICE_FUNC
  inline Derived& const_cast_derived() const
  { return *static_cast(const_cast(this)); }
  EIGEN_DEVICE_FUNC
  inline const Derived& const_derived() const
  { return *static_cast(this); }

  /** \returns the number of rows. \sa cols(), RowsAtCompileTime */
  EIGEN_DEVICE_FUNC EIGEN_CONSTEXPR
  inline Index rows() const EIGEN_NOEXCEPT { return derived().rows(); }//通过派生类的实现来实现,实际上在PlaibObjectBase中有定义rows(),通过调用DenseStorage的rows实现的
  /** \returns the number of columns. \sa rows(), ColsAtCompileTime*/
  EIGEN_DEVICE_FUNC EIGEN_CONSTEXPR
  inline Index cols() const EIGEN_NOEXCEPT { return derived().cols(); }

发现cols()rows()这些是通过派生类间接实现的,那么在派生类中就不用再次定义这些接口了。基于此,我们发现Matrix中的接口集中是一些构造接口,并非是预想的那样,里面会有好多矩阵操作的接口。实际上,Eigen正是将矩阵的各种操作进行不断的抽象,才出现我们今天看到的这么多继承等级。除此之外,EigenBase等也可以单独当作模板使用。

3.2 与普通模板的区别

普通模板虽然也定义了行为,但是行为的实现是不可变的,唯一的区别是实例化的类型不同。CRTP实例化后,每个派生类别的实现却可以不同。

template
class Base{
 void Process(){
}
};

class A{
}

class B{
}
template
class Base{
 void Process(){
    Derived().ProcessImpl();
 }

Derived& Derived(){
 return *static_cast(this);
}
};

class A:public Base
{
      
       void ProcessImpl(){
      }
}

class B:public Base
{
      
       void ProcessImpl(){
      }
}

上述两个代码片段可以看出,相比比普通模板,CRTP具有拓展派生类实现行为的能力,而不是保持基类实现行为不变。


参考资料

  1. <> second edition. David Vandevoorde, Nicolai M. Josuttis and Douglas Gregor

下一篇: