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. 2023-11-22:用go语言,给你一个长度为 n 下标从 0 开始的整数数组 nums。 它包含 1 到 n 的所有数字,请你返回上升四元组的数目。 如果一个四元组 (i, j, k, l) 满足

    2023-11-22:用go语言,给你一个长度为 n 下标从 0 开始的整数数组 nums. 它包含 1 到 n 的所有数字,请你返回上升四元组的数目. 如果一个四元组 (i, j, k, l) 满足 ...

  2. 【scipy 基础】--稀疏矩阵

    稀疏矩阵是一种特殊的矩阵,其非零元素数目远远少于零元素数目,并且非零元素分布没有规律.这种矩阵在实际应用中经常出现,例如在物理学.图形学和网络通信等领域. 稀疏矩阵其实也可以和一般的矩阵一样处理,之所 ...

  3. mac中删除本地maven库中下载失败的.lastUpdated文件

    在 macOS 中,要删除本地 Maven 仓库中所有的 .lastUpdated 文件,您可以使用 find 命令结合 rm 命令来执行这个操作.这可以在终端(Terminal)中完成. 打开您的终 ...

  4. .net下优秀的日志框架Serilog,你用上了吗?强烈推荐

    在 .NET 开发中,Serilog 是一款广受欢迎的日志库,它提供了强大的日志记录功能,具有丰富的特性和高度的可扩展性.Serilog 的优秀之处包括: 可扩展性: Serilog 可以轻松扩展以满 ...

  5. stm32存储器:Flash

    先擦除后写入,stm32内置flash擦或写时,必须打开外部/内部高速振荡器. 擦除操作 以页为单位,每页1024个字节 起始地址0x0800 0000 擦写时要避开用户程序存储区 最多擦写10万次 ...

  6. 轻量级SpringBoot配置中心 - Minimal-Config

    介绍 minimal-config-spring-boot-starter,是基于Spring-Boot原生配置注入实现原理的基础上,拓展的轻量级配置中心,项目体积只有24KB,设计理念为服务中小型项 ...

  7. springBoot——整合mybatis

    spring整合mybatis springBoot整合mybaits 配置文件 spring: datasource: url: jdbc:mysql://localhost:3306/test d ...

  8. JWT简单使用

    创建一个Maven项目,并导入jar包 <?xml version="1.0" encoding="UTF-8"?> <project xml ...

  9. 太牛叉了!国产 AI 智能体惊艳问世,全面致敬 FastGPT!

    太震撼了!太厉害了!昆仑万维正式发布了「天工 SkyAgents」平台,助力大模型走入千家万户.你听听,这个名字一听就有一种巧夺天工的感觉,技艺那是相当的高超. 这个平台基于昆仑万维「天工大模型」打造 ...

  10. 2024-01-06:用go语言,在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧 在桥上有一些石子,青蛙很讨厌踩在这些石子上 由于桥的长度和青蛙一次跳过的距离都是正整数 我们可以把独木桥

    2024-01-06:用go语言,在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧 在桥上有一些石子,青蛙很讨厌踩在这些石子上 由于桥的长度和青蛙一次跳过的距离都是正整数 我们可以把独木桥 ...