C++ CRTP 简介

奇异递归模板模式 (Curiously Recurring Template Pattern, CRTP) 是 C++ 的一种模板编程技巧(虽然不太常用就是了)。

互联网上关于 CRTP 的中文资料都是用它来“消除”动态调度的,也就是通过在基类的模板参数中传入派生类类型,以便在基类中可以直接使用派生类的成员,以此达到所谓的“消除动态调度”。可以参考( Google 搜索排行第一的)该文章的描述。实际上,这是用错了 CRTP 。虽然文章中隐约意识到了错误,但是最终还是为其打了个圆场结束了文章。

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

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

所以 CRTP 有什么用?尽管还是无法做到消除动态调度(或者说它根本就不是被用来做这个的),它是用来作代码复用的。

从某种意义上来说,奇异递归模板模式比较类似委托模式,这我是在 SF 上看了一篇文章之后领悟到的。而且较之于委托模式,C++ 的非虚继承没有任何代价,因此 CRTP 比委托模式高效。而且 C++ 中可以多继承,也就是说可以一次引入多段代码,这也是 C++ 独特的优势。

引入

回忆一下 Head first 设计模式第一章的内容(这章介绍了委托模式):我们有很多种类的鸭子需要实现,它们可能会飞、会叫,但是每种鸭子可能会不同地飞、可能会不同地叫。此时如果针对每种鸭子都实现一遍各自的飞和叫,重复代码量巨大。

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

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> {
  MallardDuck()
      : Quack{*this} {
  }
};

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

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

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

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

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

template <typename Derive>
struct Quack {
  const Derive &self;
  Quack(const Derive &self)
    : self{self} {
  }
  void quack() {
    std::cout << self.name << " quack!\n";
  }
};

template <typename Derive>
struct MuteQuack {
  const Derive &self;
  MuteQuack(const Derive &self)
    : self{self} {
  }
  void quack() {
    std::cout << self.name << " did not say any thing!\n";
  }
};

struct MallardDuck: Quack<MallardDuck> {
  const char *name;
  MallardDuck(const char *name)
    : Quack{*this}, name{name} {
    }
};

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

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

这样子,我们就将鸭子的行为模块化了。这一点和委托模式很类似。

更灵活的 CRTP

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

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

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

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

#include <iostream>

template <typename Derive>
struct Fly {
  const Derive &self;
  Fly(const Derive &self)
      : self{self} {
  }
  void fly() {
    std::cout << self.name << " flies!\n";
  }
};

// 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}
      , QuackBehavior<Duck<QuackBehavior, FlyBehavior>>{*this}
      , FlyBehavior<Duck<QuackBehavior, FlyBehavior>>{*this} {
  }
};

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

可变的 CRTP

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

回到 Head First 设计模式书中,开放-关闭原则向我们介绍了

我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。

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

从 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;
  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.
}

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

后记

最近一个月挺忙的,出去比赛,准备讲课,还有不少平时作业和期中考复习占据了大部分时间。剩下还有一点时间给自己找借口休息了一下、(正当)划水。

前些天刚好遇到祖父去世,回去祭拜了一波,顺便把某个不该出现在我祖父墓碑上的名字涂抹掉,只是之后被我爸刁难,被报警被立案,险些被送入牢房。还好在我妈的帮助下解决了问题,只是心情有点无法平静,在这上面浪费了不少时间。

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注