C++ CRTP 简介

奇异递归模板模式 (Curiously Recurring Template Pattern, CRTP) 是 C++ 的一种模板编程技巧。它最主要的作用是利用派生类的信息来生成代码,比如通过在派生类中实现模板类所要求的某个接口,只要继承这个模板类就可以自动生成所有模板类提供的接口函数。

一个典型的例子是 Boost.Operators 库。这个库实现了一系列的 concepts (话说这玩意儿在 C++17 已经改名成了 named requirements ),举例而言只要派生类 T 实现了 operator< 并继承 boost::equivalent<T> ,那么 boost::equivalent<T> 就可以利用这个 < 自动生成其它所有的比较函数。

标准库(曾经在 Boost 中实现,后来被标准化)中也有利用了 CRTP 的类 std::enable_shared_from_this ,它没有什么对派生类的具体接口要求,它仅仅是利用了派生类的类型信息以便于构造 std::shared_ptrstd::weak_ptr 而已。

拨乱

互联网上很多关于 CRTP 的中文资料都是用它来“消除”动态调度的,也就是通过在基类的模板参数中传入派生类类型,以便在基类中可以直接使用派生类的成员,以此达到所谓的“消除动态调度”。但是最后提到 CRTP 有什么用的时候,却又使用虚函数和继承来使得不同的派生对象得以保存在容器中,可以参考(我搜 Google 的时候排行第一的)该文章的描述。实际上,这是用错了 CRTP 。虽然文章中隐约意识到了错误,但是最终还是为其打了个圆场结束了文章。

在引用链接提到的文章中的情境下, CRTP 完全是没有必要的,它唯一的作用只是增加代码复杂度。

由于不同派生类的基类模板参数各不相同,因此各个基类都是不同的类型,这就是说:它们无法被统一管理、统一调度,此时如果又需要如此(比如放到容器里统一调度),文章中又要求模板基类继承自同一个总基类,将函数写成虚函数——它最终还是没能避免动态调度,反而平添了代码复杂度。

所以 CRTP 根本不是用来解决统一调度和管理派生类的,如果需要统一调度和管理某些类却又想避免虚表,那你应该用 std::variant

基本上可以说,提到利用 CRTP 消除虚表实现多态的文章作者,都没有真正理解 CRTP 。

引入

回忆一下委托模式的一个例子:我们有很多种类的鸭子需要实现,它们可能会飞、会叫,但是每种鸭子可能会不同地飞、可能会不同地叫。此时如果针对每种鸭子都实现一遍各自的飞和叫,重复代码量巨大,特别是当需要修改的时候也会有大量重复的工作。

将“飞”和“叫”两种行为包装成类,各自分别实现自一个接口。通过在真实的鸭子类中实例化各种鸭子对应的不同行为,将“飞”和“叫”的行为委托给这些实例委托类实现即可。

interface QuackBehavior {
  public void quack();
}

class Quack implements QuackBehavior {
  public void quack() {
    System.out.println("Quack");
  }
}

class MuteQuack implements QuackBehavior {
  public void quack() {
    System.out.println("(Silence)");
  }
}

public abstract class Duck {
  QuackBehavior quackBehavior;
  public Duck(QuackBehavior quackBehavior) {
    this.quackBehavior = quackBehavior;
  }
  public void quack() {
    this.quackBehavior.quack();
  }
}

class MallardDuck extends Duck {
  public MallardDuck() {
    super(new Quack());
  }
}

class RubberDuck extends Duck {
    public RubberDuck() {
      super(new MuteQuack());
    }
}

C++ 中当然也可以用这种方案来实现代码复用,但是这个方案依赖于动态调度,“飞”和“叫”两种行为需要各自继承自一个基类并实现虚函数。

如果这些行为在编译期就可以确定下来,运行时不需要再修改,不需要在运行时构造函数中绑定行为,那么 CRTP 就可以派上用场。

简介

template <typename Derive>
struct Quack {
  void quack() {
    std::cout << "Quack\n";
  }
};

template <typename Derive>
struct MuteQuack {
  void quack() {
    std::cout << "(Silence)\n";
  }
};

struct MallardDuck: Quack<MallardDuck> {
};

这是最简单的,也是最废柴的例子。很简单,只要公开继承就可以获得父类的函数了。

所谓的 CRTP ,就是基类作为模板类,派生类在继承基类的时候,传入自己作为模板参数。

在类被实际定义前就作为它本身基类的模板参数,这种看起来很奇异的做法实际上是合法的。也正因此,这种方式被叫做奇异递归模板模式。

但是遗憾的是,上面那个例子并不能说明 CRTP 的作用,因为这里即使不用 CRTP ,简单地写一个非模板类并从中派生也可以做到这种程度的代码复用。

CRTP 真正的用武之地,是在模板类需要访问派生的类的成员(变量或函数)的时候,它可以引用它的派生类,也就可以访问派生类的成员。

template <typename Derive>
struct Quack {
  void quack() {
    std::cout
<< static_cast<Derive *>(this)->name << " quack!\n"; } }; template <typename Derive> struct MuteQuack { void quack() { std::cout
<< static_cast<Derive>(this)->name << " did not say any thing!\n"; } }; struct MallardDuck: Quack<MallardDuck> { const char *name; MallardDuck(const char *name): name{name} { } };

如上例中,基类使用了派生类的 name 成员,用于在 quack 成员函数中输出 name 。如果我们需要另一种鸭子类,它的 quack 行为是 MuteQuack 的行为,则它可以直接继承 MuteQuack 的模板实例。

如果说我们再加入一个飞行行为,我们只需要类似地在一个新的模板基类中实现“飞”的行为,再让我们的 MallardDuck 继承自它就好了。

这样子,我们就将鸭子的行为模块化了。

那么实际上到此为止,已经满足了上文提到的 boost::equivalentstd::enable_shared_from_this 的需求,我们已经可以实现 CRTP 了。下面的段落,则是 CRTP 在另一个方向上的变化。

更灵活的 CRTP

事实上,我们可以将几个模板基类看作零件,如果我们需要组装出一只鸭子——我们不需要在意具体是绿头鸭、还是橡皮鸭。我们只关心它是由什么行为“组成”的——我们可以将派生类也变成模板类,将它的零件作为模板参数,然后继承自模板参数即可。

其实这是个很刁钻的需求,父类需要子类的类型信息作为模板参数才能构建,子类又需要父类作为模板参数……这岂不是真的陷入了类型的递归?

这时候 template template parameter 就派上用场了。由于我们的基类是一个已知的模板参数,只要在继承中使用模板参数替代实际的基类类型名即可。

比如说之前的 MallardDuck ,我们现在不管它到底是不是所谓的 Mallard Duck ,我现在只需要它会飞、会叫,用 CRTP 配合上 template template parameter 这么实现:

#include <iostream>

template <typename Derive>
struct Fly {
  void fly() {
    std::cout
      << static_cast<Derive *>(this)->name
      << " flies!
";
  }
};

// A same class Quack implementation as above.

template <template <typename> class QuackBehavior, template <typename> class FlyBehavior>
struct Duck
    : QuackBehavior<Duck<QuackBehavior, FlyBehavior>>
    , FlyBehavior<Duck<QuackBehavior, FlyBehavior>> {
  const char *name;
  Duck(const char *name): name{name} {
  }
};

int main() {
  Duck<Quack, Fly> xris{"Xris"};
  xris.quack();
  xris.fly();
}

于是,一只既会飞,也会叫的 xris 横空出世。只可惜 RealWorld 的 xris 只会叫,不会飞。

可变的 CRTP

有些时候,我们会遇到某些种类的鸭子,它可能不会飞,于是我就不为它实现 fly 成员函数(然后用于 SFINAE );或者有可能,我们需要为某些鸭子类增添一个属性: SwimBehavior 。这些时候,鸭子类只能使用固定的几个模板参数就显得很笨拙。

为了达到这个目的,我们需要借用可变参数的力量。

从 C++11 开始,标准引入了可变参数模板,模板中可以传入任意数量的参数,利用参数包解包,可以很方便地一次性继承多个类。

template <template <typename ...> class ...Impl>
struct Duck: public Impl<Duck<Impl...>>... {
  const char *name;
  Duck(const char *name)
      : name{name}, Impl<Duck<Impl...>>{*this}... {
  }
};

这便是集大成的鸭子类,我们剩下来需要做的就是对于每种鸭子的行为编写具体实现。这些“行为”可能是飞、跑、游泳、叫,甚至和鸭子一点关系都没有的引用计数、相等关系、四则运算等等,这实际上已经超出了具体的某个(鸭子)类型,可以泛化为所谓的“物件”了。

template <typename Derive>
struct Swim {
  const Derive &self;  // We have self here to avoid static_cast
  Swim(const Derive &self)
      : self{self} {
  }
  void swim() {
    std::cout << self.name << " swims!\n";
  }
};

int main() {
  Duck<Quack, Fly, Swim> duck0;  // A duck that can quack, fly and swim.
  Duck<Quack, Fly> duck1;  // A duck that cannot swim.
  Duck<Quack> duck2;  // A duck that can only quack.
}

まったく、小黄鸭は最高だぜ!!

后记

最近一个月挺忙的,外出搬砖好几周,剩下还有一点时间给自己找借口休息了一下、(正当)划水。

结果前些天刚好遇到祖父去世,被叫回老家守灵,然后又出了一些事情,只是之后被我爸刁难,被报警被立案,五味杂陈,没缓过来。

不想提太多家里的事情,这之后的作息应该会恢复正常,安心好好读书吧,不要与人渣怄气。

One Reply to “C++ CRTP 简介”

  1. 更灵活的 CRTP的例子如何约束一组基类的函数名(函数接口)是统一的

发表评论