【C++奇技淫巧】CRTP(奇特重现模板模式)

2024-05-31 18:52

本文主要是介绍【C++奇技淫巧】CRTP(奇特重现模板模式),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

CRTP(Curiously Recurring Template Pattern,奇特重现模版模式),是一种在C++中使用模板来实现的设计模式,主要用于实现编译时多态性(静态多态)。这种模式通过类模板和模板继承机制来实现,使得派生类在继承时将自身作为基类模板的参数。

这里采用的中文名字参考了zh.cppreference.com

示例解析

template <typename T>
class Base {
public:void doSomething(){// 访问 Derived 类的成员static_cast<T*>(this)->implementation();}
};class Derived : public Base<Derived> {
public:void implementation(){std::cout << "Derived::implementation()";}
};int main()
{Derived d;d.doSomething();return 0;
}

输出结果:

Derived::implementation()

解析

编译器在编译上面代码时,因为延迟实例化,所以在Derived派生类后面实例化Base<Derived>
在实例化的Base<Derived>类的doSomething()函数中,将this指针从Base<Derived>*转换成Derived*,这样就能访问Derived类中定义的方法了。

CRTP的多态性

C++多态的实现方式分为动态多态(虚函数)、静态多态(模板、函数重载)。
其中用模板实现静态多态的方式也有很多种:

  1. 函数模板
  2. 类模板
  3. CRTP(通过模板继承实现)

动态多态和静态多态

动态多态

动态多态是通过虚函数来实现的。它允许在运行时确定调用哪个对象/方法,这主要依赖于虚函数表(vtable)来动态解析调用。

优点:
  1. 灵活性:可以在运行时改变对象的行为
  2. 易于使用:只需声明虚函数,让派生类重写函数即可
缺点:
  1. 性能开销:每次调用虚函数时都需要通过虚指针转到虚函数表,再找到对应的函数,花费时间长
  2. 内存开销:每个对象都需要额外的内存来存储指向虚函数表的指针。

静态多态

静态多态通常是通过模板实现的,它在编译时就明确了调用的对象/方法,而不是在运行时。

优点:
  1. 性能开销:没有运行时的多余开销,函数调用可以直接解析
  2. 内存开销:运行时不会有多余的内存开销
缺点:
  1. 代码膨胀:对于每个不同的类型,编译器可能都会生成一份代码,这可能导致最终编译出的二进制文件体积增大
  2. 灵活性较低:在编译时就已经确定了所有的类型,不能像动态多态那样在运行时改变对象的行为

CRTP和虚函数

性能与开销

  • CRTP:使用 CRTP 实现的是静态多态,即在编译时就解析函数调用,而不需要虚函数表,从而避免了运行时的查找开销。这种方法通过模板实例化直接绑定函数,因此运行效率更高,没有额外的内存开销。
  • 虚函数:动态多态通过虚函数实现。虚函数依赖于虚函数表(vtable),这意味着每次调用虚函数时都需要通过虚函数表进行间接跳转。这种间接性带来运行时开销,同时每个使用虚函数的对象需要额外的内存来存储指向虚函数表的指针。

灵活性和可维护性

  • CRTP:CRTP 允许基类通过模板机制访问派生类的成员,这样可以将一些通用的功能封装在基类中,而具体的实现则在派生类中完成。CRTP 特别适合于实现混入(Mixins)类型的功能,能够在不修改原有类结构的情况下,为类增加额外的功能。
  • 虚函数:虚函数提供了极高的灵活性,允许在运行时决定对象的行为。这使得代码可以容易地扩展和修改,但可能导致代码维护和理解的复杂度增加。

代码重用与扩展

  • CRTP:CRTP 允许基类通过模板机制访问派生类的成员,这样可以将一些通用的功能封装在基类中,而具体的实现则在派生类中完成。CRTP 特别适合于实现混入(Mixins)类型的功能,能够在不修改原有类结构的情况下,为类增加额外的功能。

上面提到的混入(Mixins)指的是在面向对象编程中用于增加类功能的技术,它通过多重继承将功能模块(Mixin类)混入到一个类中。这种方法允许程序员在不修改原始类代码的情况下为类添加新的行为和属性。

  • 虚函数:通过虚函数,派生类可以覆盖基类中的行为,实现功能的自定义和扩展。这种方法简洁直观,易于理解和使用。

类型安全和错误检测

  • CRTP:由于 CRTP 是在编译时处理的,所以相关的类型错误会在编译阶段被捕捉和报告,增加了开发过程中的类型安全性。
  • 虚函数:虚函数的错误(如类型不匹配)通常在运行时发现,有时这可能导致程序崩溃或不稳定。

CRTP的案例展示

enable_shared_from_this

enable_shared_from_this就是一个典型的CRTP案例,继承enable_shared_from_this类的对象可以从内部生成std::shared_ptr

这个模板类最初是Boost库引入的,后面被纳入到C++标准库中,下面我们展示Boost库下的源码

namespace boost
{template<class T> class enable_shared_from_this
{
protected:BOOST_CONSTEXPR enable_shared_from_this() BOOST_SP_NOEXCEPT{}BOOST_CONSTEXPR enable_shared_from_this(enable_shared_from_this const &) BOOST_SP_NOEXCEPT{}enable_shared_from_this & operator=(enable_shared_from_this const &) BOOST_SP_NOEXCEPT{return *this;}~enable_shared_from_this() BOOST_SP_NOEXCEPT // ~weak_ptr<T> newer throws, so this call also must not throw{}public:shared_ptr<T> shared_from_this(){shared_ptr<T> p( weak_this_ );BOOST_ASSERT( p.get() == this );return p;}shared_ptr<T const> shared_from_this() const{shared_ptr<T const> p( weak_this_ );BOOST_ASSERT( p.get() == this );return p;}weak_ptr<T> weak_from_this() BOOST_SP_NOEXCEPT{return weak_this_;}weak_ptr<T const> weak_from_this() const BOOST_SP_NOEXCEPT{return weak_this_;}public: // actually private, but avoids compiler template friendship issues// Note: invoked automatically by shared_ptr; do not calltemplate<class X, class Y> void _internal_accept_owner( shared_ptr<X> const * ppx, Y * py ) const BOOST_SP_NOEXCEPT{if( weak_this_.expired() ){weak_this_ = shared_ptr<T>( *ppx, py );}}private:mutable weak_ptr<T> weak_this_;
};} // namespace boost

下面展示使用标准库中的enable_shared_from_this示例:

#include <memory>
#include <iostream>class MyClass : public std::enable_shared_from_this<MyClass> {
public:std::shared_ptr<MyClass> get_shared_ptr() {return shared_from_this();}
};int main() {std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();std::shared_ptr<MyClass> another_ptr = ptr->get_shared_ptr();std::cout << "Same object: " << (ptr == another_ptr) << std::endl;
}

日志类

template<typename T>
class Logging {
public:void log(const std::string& message) const {// 获取派生类的名称,这里假设派生类有一个名为 getName 的方法std::cout << static_cast<const T*>(this)->getName() << ": " << message << std::endl;}
};class Car : public Logging<Car> {
public:Car(const std::string& model) : model_(model) {}std::string getName() const { return model_; }void drive() {this->log("Starting the engine.");// 驾驶逻辑...this->log("Stopped the engine.");}private:std::string model_;
};class Robot : public Logging<Robot> {
public:Robot(const std::string& identifier) : identifier_(identifier) {}std::string getName() const { return identifier_; }void operate() {this->log("Activating robot.");// 操作逻辑...this->log("Deactivating robot.");}private:std::string identifier_;
};int main() {Car car("Toyota Camry");car.drive();Robot robot("Android 007");robot.operate();return 0;
}

总结

我总结了CRTP的具体应用场景:

  1. 使用虚函数有性能问题,需要优化,可以将虚函数转成CRTP(后续文章会介绍)
  2. 能够抽象出通用方法,派生类只使用方法,不重写/修改方法
  3. 需要基于现有的通用方法来添加新功能(不改变通用方法)

其实在使用CRTP和虚函数的核心区别是,CRTP的继承关系中能够抽象出通用方法,且不会修改通用方法;而虚函数是抽象出通用方法,但要重写方法,只是把通用方法当做一个入口。

这篇关于【C++奇技淫巧】CRTP(奇特重现模板模式)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1018760

相关文章

Java设计模式---迭代器模式(Iterator)解读

《Java设计模式---迭代器模式(Iterator)解读》:本文主要介绍Java设计模式---迭代器模式(Iterator),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录1、迭代器(Iterator)1.1、结构1.2、常用方法1.3、本质1、解耦集合与遍历逻辑2、统一

Java 线程安全与 volatile与单例模式问题及解决方案

《Java线程安全与volatile与单例模式问题及解决方案》文章主要讲解线程安全问题的五个成因(调度随机、变量修改、非原子操作、内存可见性、指令重排序)及解决方案,强调使用volatile关键字... 目录什么是线程安全线程安全问题的产生与解决方案线程的调度是随机的多个线程对同一个变量进行修改线程的修改操

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性:

c++中的set容器介绍及操作大全

《c++中的set容器介绍及操作大全》:本文主要介绍c++中的set容器介绍及操作大全,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录​​一、核心特性​​️ ​​二、基本操作​​​​1. 初始化与赋值​​​​2. 增删查操作​​​​3. 遍历方

解析C++11 static_assert及与Boost库的关联从入门到精通

《解析C++11static_assert及与Boost库的关联从入门到精通》static_assert是C++中强大的编译时验证工具,它能够在编译阶段拦截不符合预期的类型或值,增强代码的健壮性,通... 目录一、背景知识:传统断言方法的局限性1.1 assert宏1.2 #error指令1.3 第三方解决

C++11委托构造函数和继承构造函数的实现

《C++11委托构造函数和继承构造函数的实现》C++引入了委托构造函数和继承构造函数这两个重要的特性,本文主要介绍了C++11委托构造函数和继承构造函数的实现,具有一定的参考价值,感兴趣的可以了解一下... 目录引言一、委托构造函数1.1 委托构造函数的定义与作用1.2 委托构造函数的语法1.3 委托构造函

C++11作用域枚举(Scoped Enums)的实现示例

《C++11作用域枚举(ScopedEnums)的实现示例》枚举类型是一种非常实用的工具,C++11标准引入了作用域枚举,也称为强类型枚举,本文主要介绍了C++11作用域枚举(ScopedEnums... 目录一、引言二、传统枚举类型的局限性2.1 命名空间污染2.2 整型提升问题2.3 类型转换问题三、C

C++链表的虚拟头节点实现细节及注意事项

《C++链表的虚拟头节点实现细节及注意事项》虚拟头节点是链表操作中极为实用的设计技巧,它通过在链表真实头部前添加一个特殊节点,有效简化边界条件处理,:本文主要介绍C++链表的虚拟头节点实现细节及注... 目录C++链表虚拟头节点(Dummy Head)一、虚拟头节点的本质与核心作用1. 定义2. 核心价值二