From Wikipedia,

The curiously recurring template pattern (CRTP) is an idiom, originally in C++, in which a class X derives from a class template instantiation using X itself as a template argument.[1] More generally it is known as F-bound polymorphism, and it is a form of F-bounded quantification.

CRTP 全称是 Curious Recurring Template Pattern,是一种 CXX 的设计模式,精巧地结合了继承和模板编程的技术。可以用来给 Cpp 的 Class 提供额外的功能、实现静态多态等。

虚函数与动态绑定

C++ 通过类的继承与虚函数的动态绑定,实现了多态。这种特性,使得我们能够用基类的指针,访问子类的实例。例如我们可以实现一个名为 Animal 的基类,以及 Cat, Dog 等子类,并通过在子类中重载虚函数 jump,实现不同动物的跳跃动作。而后我们可以通过访问 Zoo 类的实例中存有 Animal 指针的数组,让动物园中所有的动物都跳一遍。

class Zoo {
...
private:
std::vector<shared_ptr<Animal>> animals;
public:
void () {
for (auto animal : animals) {
animal->jump();
}
}
...
}

在每次执行 animal->jump() 的时候,系统会检查 animal 指向的实例实际的类型,然后调用对应类型的 jump 函数。这一步骤需要通过查询虚函数表(vtable)来实现;由于实际 animal 指向对象的类型在运行时才确定(而不是在编译时就确定),所以这种方式称为动态绑定(或者运行时绑定)。

因为每次都需要查询虚函数表,所以动态绑定会降低程序的执行效率。为了兼顾多态与效率,有人提出了 Curiously Recurring Template Pattern 的概念。

通过模板实现静态绑定

为了在编译时绑定,我们就需要放弃 C++ 的虚函数机制,而只是在基类和子类中实现同名的函数;同时,为了在编译时确定类型,我们就需要将子类的名字在编译时提前传给基类。因此,我们需要用到 C++ 的模板。

// demo.cpp
#include <iostream> template<typename T>
class Base {
public:
void show() const {
static_cast<const T *>(this)->show();
}
}; class Derived : public Base<Derived> {
public:
void show() const {
std::cout << "Shown in Derived class." << '\n';
}
}; int main() {
Derived d;
Base<Derived> *b = &d;
b->show();
return 0;
}

这是一个简单的 CRTP 的例子,有以下一些特点:

  • 基类是一个模板类,接收子类的类型名字;
  • 因此子类的继承列表会类似于 Derived: public Base<Derived>;
  • 基类的函数在函数体中,使用 static_cast<> 将基类的指针转为(模板)子类的指针,在编译期完成绑定。

因此,在实际执行时,我们用 b->show() 打印出「Shown in Derived class.」的字样,显示我们确实调用了子类的 show 函数。

再举一个稍微复杂一点的例子。

// complicated_demo.cpp
// Please use gcc
#include <iostream> template<typename T>
class Base {
public:
void show() const {
static_cast<const T *>(this)->show();
} Base<T> operator++() {
static_cast<T *>(this)->operator++();
}
}; class Derived : public Base<Derived> {
public:
Derived() : val(0) {}; void show() const {
std::cout << "Shown in Derived class." << '\n';
std::cout << "val is: " << val << '\n';
} Derived operator++() {
++(this->val);
return *this;
} private:
int val;
}; int main() {
Derived d;
Base<Derived> * b = &d;
b->show();
++(*b);
b->show();
return 0;
}

这一次,我们在子类中,额外重载了前置的自增运算符(参数列表不带 int)。因此,在基类中,我们首先要将 this 指针转换为 T* 类型,然后调用子类的前置自增运算符(operator++())。

用在哪里?

现在我们考虑这样一个问题。

在使用虚函数的风格中,我们可以把 Cat*, Dog*... 等不同子类的指针,复制给基类的指针 Animal*,然后把基类的指针存入容器中(比如 vector<Animal*>)。但是,在 CRTP 中,我们就做不到这样了。这是因为同样是基类的指针 Animal<Cat>* 和 Animal<Dog>* 是两种完全不同的类型的指针。这样一来,我们就没法构造一个动物园了。

摔!

那么,CRTP 到底应该怎么用呢?我们不妨回过头来想一想,最初我们引入 CRTP 是为了什么。文章开头的第一段,我们提到多态是个很好的特性,但是动态绑定比较慢,因为要查虚函数表。而事实上,动态绑定慢,通常是因为多级继承;如果继承很短,那么查虚函数表的开销实际上也没多大。

在之前举出的例子里,我们运用 CRTP,完全消除了动态绑定;但与此同时,我们也在某种意义上损失了多态性。现在我们希望二者兼顾:保留多态性,同时降低多级继承带来的虚函数表查询开销。答案也很简单:让 CRTP 的模板类继承一个非模板的基类——这相当于这个非模板的基类会有多个平级的不同的子类。一个示例如下。

#include <iostream>
#include <vector> using std::cout;
using std::vector; class Animal {
public:
virtual void say() const = 0; virtual ~Animal() {}
}; template<typename T>
class Animal_CRTP : public Animal {
public:
void say() const override {
static_cast<const T *>(this)->say();
}
}; class Cat : public Animal_CRTP<Cat> {
public:
void say() const {
cout << "Meow~ I'm a cat." << '\n';
}
}; class Dog : public Animal_CRTP<Dog> {
public:
void say() const {
cout << "Wang~ I'm a dog." << '\n';
}
}; int main() {
vector<Animal *> zoo;
zoo.push_back(new Cat());
zoo.push_back(new Dog());
for ( vector<Animal *>::const_iterator iter{ zoo.begin() }; iter != zoo.end(); ++iter )
{
(*iter)->say();
}
for ( vector<Animal *>::iterator iter{ zoo.begin() }; iter != zoo.end(); ++iter )
{
delete (*iter);
}
return 0;
}

这样一来,我们就兼顾了多态性和效率。

References

Cpp 惯用法 CRTP 简介的更多相关文章

  1. RAII惯用法:C++资源管理的利器(转)

    RAII惯用法:C++资源管理的利器 RAII是指C++语言中的一个惯用法(idiom),它是“Resource Acquisition Is Initialization”的首字母缩写.中文可将其翻 ...

  2. 做个地道的c++程序猿:copy and swap惯用法

    如果你对外语感兴趣,那肯定听过"idiom"这个词.牛津词典对于它的解释叫惯用语,再精简一些可以叫"成语".想要掌握一门语言,其中的"成语" ...

  3. Erase-Remove 惯用法

    看到<Effective STL>条款 9 的时候想到了我以前复习的"如何正确使用迭代器删除元素",我面试时使用的也是里面的方法,看面试官的反应好像也没有什么问题,还问 ...

  4. C++惯用法:通过成员模板实现隐式转换(Coercion 强迫 by Member Template)

    Intent To increase the flexibility of a class template's interface by allowing the class template to ...

  5. ibatis.net:惯用法

    使用<![CDATA[]]>保持SQL格式 IN 查询

  6. (一)Thymeleaf用法——Thymeleaf简介

    1. thymeleaf认识 参考官方文档(Project version: 3.0.5.RELEASE)   1.1 介绍 Thymeleaf是面向Web和独立环境的现代服务器端Java模板引擎,能 ...

  7. C++之RAII惯用法

    http://blog.csdn.net/hunter8777/article/details/6327704 C++中的RAII全称是“Resource acquisition is initial ...

  8. Python惯用法

    目录 1. 不要使用可变类型作为参数的默认值 1. 不要使用可变类型作为参数的默认值 摘自<流畅的Python>8.4.1 class HauntedBus: ""&q ...

  9. C++ Curiously Recurring Template Prattern(CRTP)例程

    简单介绍和例子请参考:C++ 惯用法 CRTP 简介 下面例子为兼顾CRTP和多态的例子. #include <iostream> #include <vector> usin ...

  10. Golang channel 用法简介

    channel 是 golang 里相当有趣的一个功能,大部分时候 channel 都是和 goroutine 一起配合使用.本文主要介绍 channel 的一些有趣的用法. 通道(channel), ...

随机推荐

  1. 【web实验报告】实验二

    一.实验目的 通过一个小型网站的开发,掌握JSP基础知识,加深对session,request,response,cookie等对象的理解,掌握其使用方法,进一步深入掌握HTML.CSS和JavaSc ...

  2. 2020ICPC上海 C题(数位dp, 记忆化搜索)

    先复习了下之前做的数位DP又做了道新题才看的这道题,对我来说还是一种新类型,涉及到非线性计算,之前做的都是形如 \(dp[x]-dp[y]\)这样的只用处理一个上限做下差即可.一开始想分别枚举 \(x ...

  3. [清华集训2017] Hello World!

    Hello world! 题目背景 不远的一年前,小 V 还是一名清华集训的选手,坐在机房里为他已如风中残烛的OI 生涯做最后的挣扎.而如今,他已成为了一名光荣的出题人.他感到非常激动,不禁感叹道: ...

  4. liunx系统sed命令使用

    增 sed '$行数i 新增内容' 文件名在文件里某行上面新增内容 sed '$行数a 新增内容' 文件名在文件里某行下面新增内容 -i.bak对源文件进行修改并备份修改之前源文件 #在文件里第5行上 ...

  5. 解决yolo+cudnn+opencv+gpu的一些问题

    问题描述: 在makefile文件中修改GPU=1 CUDNN=1 OPENCV=1,然后重新make,执行命令时出现: Yolov3 darknet: ./src/cuda.c:36: check_ ...

  6. SpringBoot内容协商机制

    1.是什么? SpringBoot内容协商机制是一种实现了内容协商(Content Negotiation)的Web服务器,它可以根据客户端请求的不同,将响应返回给客户端. 在传统的Web服务器中,如 ...

  7. CAP 8.0 版本发布通告 - CAP 7岁生日快乐!

    前言 今天,我们很高兴宣布 CAP 发布 8.0 版本正式版,从 2016 年 12 月 14 日CAP立项到 2023 年 12 月14 日发布 8.0 版本刚好满 7 年,祝 CAP 7 岁生日快 ...

  8. 通过工厂函数封装返回app对象

    main.py #主文件 import os from app import create_app # 通过环境变量设置项目运行时使用的配置文件,这里就手动设置以下了,一般部署的时候通过脚本等设置. ...

  9. 98秒转录2.5小时音频,最强音频翻译神器IFW下载部署

    IFW是一款功能强大的音频翻译工具,具备高速转录能力,能在短时间内完成大量音频处理,提高工作效率 以下是IFW在 Nvidia A100 - 80GB 上运行的一些基准测试: 最新中文版:https: ...

  10. Python——第一章:for循环

    字符串是可迭代的for循环: for 任意变量名 in 字符串|列表|元祖|字典|集合(可迭代的东西): 代码 for循环把可迭代的东西中的每一项内容拿出来. 挨个的赋值给变量. 每一次赋值都要执行一 ...