【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

相关文章

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

IDEA自动生成注释模板的配置教程

《IDEA自动生成注释模板的配置教程》本文介绍了如何在IntelliJIDEA中配置类和方法的注释模板,包括自动生成项目名称、包名、日期和时间等内容,以及如何定制参数和返回值的注释格式,需要的朋友可以... 目录项目场景配置方法类注释模板定义类开头的注释步骤类注释效果方法注释模板定义方法开头的注释步骤方法注

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

C++ vector的常见用法超详细讲解

《C++vector的常见用法超详细讲解》:本文主要介绍C++vector的常见用法,包括C++中vector容器的定义、初始化方法、访问元素、常用函数及其时间复杂度,通过代码介绍的非常详细,... 目录1、vector的定义2、vector常用初始化方法1、使编程用花括号直接赋值2、使用圆括号赋值3、ve

如何高效移除C++关联容器中的元素

《如何高效移除C++关联容器中的元素》关联容器和顺序容器有着很大不同,关联容器中的元素是按照关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的,本文介绍了如何高效移除C+... 目录一、简介二、移除给定位置的元素三、移除与特定键值等价的元素四、移除满足特android定条件的元

Python获取C++中返回的char*字段的两种思路

《Python获取C++中返回的char*字段的两种思路》有时候需要获取C++函数中返回来的不定长的char*字符串,本文小编为大家找到了两种解决问题的思路,感兴趣的小伙伴可以跟随小编一起学习一下... 有时候需要获取C++函数中返回来的不定长的char*字符串,目前我找到两种解决问题的思路,具体实现如下:

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

C/C++错误信息处理的常见方法及函数

《C/C++错误信息处理的常见方法及函数》C/C++是两种广泛使用的编程语言,特别是在系统编程、嵌入式开发以及高性能计算领域,:本文主要介绍C/C++错误信息处理的常见方法及函数,文中通过代码介绍... 目录前言1. errno 和 perror()示例:2. strerror()示例:3. perror(