Bootstrap

CRTP编程模式(奇异递归模板)实现客户端的https管理模块

一、什么是 CRTP(Curiously Recurring Template Pattern)?

CRTP 是 C++ 中的一种编程技巧,它利用模板机制来实现静态多态性。

简单来说,CRTP 是指派生类在继承基类时,将自身作为模板参数传递给基类。通过这种方式,基类可以通过模板方法访问派生类的具体实现,从而实现类似多态的效果,但它是在编译时确定的,而不是运行时。  

CRTP 的核心思想:

  • 基类接受派生类作为模板参数:基类定义为一个模板类,接收一个模板参数(即派生类),而这个派生类在继承时会将自己作为该模板类的参数传递。
  • 静态多态(避免使用虚函数):派生类在编译时通过模板机制提供自己的具体实现,基类无需虚函数,也不会发生动态绑定。

二、为什么需要 CRTP?

  • 性能优化:传统的多态机制(通过虚函数)需要在运行时通过虚函数表(vtable)来实现,这带来了额外的性能开销。CRTP 通过静态多态性(在编译时决定类型)避免了这种运行时开销
  • 编译时决策:CRTP 允许我们在编译时就确定类型,并在基类中通过模板推导使用派生类的方法。这使得在编译时就可以进行类型特化和优化。
  • 代码复用:通过 CRTP,基类可以复用不同派生类的代码逻辑,同时保持每个派生类的特定实现,这避免了代码的重复编写。

三、CRTP 有什么作用?

  • 静态多态性:CRTP 提供了一种机制,使得基类可以调用派生类的方法,而无需使用虚函数。这样就避免了虚函数表的开销。
  • 避免运行时开销:通过在编译时确定类型,CRTP 可以避免动态绑定和运行时的开销,提高代码的性能。
  • 代码复用与扩展:基类可以提供一些通用功能,而派生类通过 CRTP 在编译时提供定制化的实现。这样既避免了每个派生类重复实现相同功能,又能根据需要定制化派生类行为。

四、怎么实现 CRTP?

CRTP 是通过模板类来实现的,基类将派生类作为模板参数。

基类通过调用 static_cast<Derived*>(this) 来访问派生类的特定实现。

#include <iostream>

// 基类模板,接收派生类作为模板参数
template <typename Derived>
class Base {
public:
    // 调用派生类的实现
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }

    // 其他通用功能
    void common_function() {
        std::cout << "Base common functionality" << std::endl;
    }
};

// 派生类
class Derived : public Base<Derived> {
public:
    void implementation() {
        std::cout << "Derived class implementation" << std::endl;
    }
};

int main() {
    Derived d;
    d.interface();        // 调用派生类的实现
    d.common_function();  // 调用基类的通用功能
    return 0;
}
  • Base<Derived>:基类模板接收派生类类型 Derived 作为模板参数。
  • static_cast<Derived*>(this):基类通过 static_cast 调用派生类的方法。
  • interface():基类的方法,在基类中调用派生类的 implementation() 方法。

五、CRTP 应用场景

  • 静态多态性:在需要多态行为时,但又不希望引入虚函数和运行时开销的场合,CRTP 可以替代虚函数机制。例如,替代传统的虚函数实现的策略模式、命令模式等。

  • 模板元编程:CRTP 可与模板元编程结合使用,在编译时根据不同的类型生成不同的代码。通过 CRTP,可以使得派生类的特性在编译时得到充分利用,提高编译时优化。

  • 类型特化:CRTP 允许在基类中根据派生类的类型特化行为,提供定制化的实现。

  • 实现策略模式:CRTP 是一种很好的实现策略模式的方式,基类可以包含通用的处理逻辑,而派生类只需提供具体的策略实现。

  • 静态接口检查:CRTP 可以用来在编译时检查派生类是否实现了必要的接口方法。例如,可以通过 CRTP 实现类似概念(concept)的功能,在编译时检查派生类是否实现了某些方法或满足某些要求。

六、CRTP 的优势

  • 高效的性能:由于是编译时多态,CRTP 避免了虚函数表(vtable)和动态绑定的运行时开销,因此在性能上比传统的虚函数多态性有显著提升。

  • 减少运行时开销:使用 CRTP,不会发生动态派发和虚函数调用,因此可以显著减少运行时开销。

  • 灵活的接口和行为扩展:CRTP 允许基类在编译时将不同的派生类行为结合在一起,而无需修改基类代码。

  • 减少代码重复:基类可以定义通用的逻辑,派生类只需专注于特定的实现,这样可以减少代码重复。

  • 类型安全:由于 CRTP 使用模板,派生类的类型信息在编译时就可获得,避免了运行时类型不匹配的问题。

七、CRTP 与模板元编程的关系

CRTP 与模板元编程关系密切,因为 CRTP 本质上是利用 C++ 的模板系统来进行编译时类型推导和类型特化。模板元编程强调在编译时根据类型信息生成代码,而 CRTP 通过将派生类作为模板参数传递给基类来实现静态多态性,符合模板元编程的思想。

模板元编程与 CRTP 的结合使用:

  • 类型推导:在 CRTP 中,基类可以通过模板推导和类型特化来根据派生类的具体类型选择不同的代码路径,这与模板元编程中的类型推导非常相似。
  • 静态断言和类型检查:通过 CRTP,编译器可以检查派生类是否符合某些接口或类型要求,这也常用于模板元编程中。
  • 优化:通过 CRTP,可以实现基于类型信息的优化,比如通过模板特化来选择合适的算法,而这种优化通常是模板元编程的一部分。

八、实例

基于Qt实现客户端https管理模块,我们需要发出网络请求,因此可以使用奇异递归模板实现,下面是具体代码

#ifndef CRTP_SINGLETON_H
#define CRTP_SINGLETON_H
#include <iostream>
#include <memory>
#include <mutex>

/******************************************************************************
 *
 * @file       crtp_singleton.h
 * @brief      单例模式的 CRTP 基类
 *
 * @author     goodsnack
 * @date       2025/01/07
 * @history
 *****************************************************************************/

// CRTP 基类,用于实现静态多态性
template <typename Derived>
class SingletonBase {
protected:
    SingletonBase() = default;
    SingletonBase(const SingletonBase&) = delete;
    SingletonBase& operator=(const SingletonBase&) = delete;

public:
    // 公共方法,用于获取实例
    static std::shared_ptr<Derived> GetInstance() {
        static std::once_flag s_flag;
        std::call_once(s_flag, [&]() {
            _instance = std::shared_ptr<Derived>(new Derived);
        });
        return _instance;
    }
    
    // 派生类特有的方法,使用 CRTP 进行静态多态调用
    void PrintAddress() {
        std::cout << _instance.get() << std::endl;
    }

    virtual ~SingletonBase() {
        std::cout << "Singleton is being destroyed" << std::endl;
    }

private:
    static std::shared_ptr<Derived> _instance;
};

// 静态成员初始化
template <typename Derived>
std::shared_ptr<Derived> SingletonBase<Derived>::_instance = nullptr;

#endif // CRTP_SINGLETON_H

#ifndef HTTPMGR_H
#define HTTPMGR_H
#include "crtp_singleton.h"
#include <QString>
#include <QUrl>
#include <QObject>
#include <QNetworkAccessManager>
#include <QJsonObject>
#include <QJsonDocument>

/******************************************************************************
 *
 * @file       httpmgr.h
 * @brief      https 管理模块
 *
 * @author     goodsnack
 * @date       2025/01/07
 * @history
 *****************************************************************************/

class HttpMgr : public QObject,
                public SingletonBase<HttpMgr>, // 继承自 CRTP 单例基类
                public std::enable_shared_from_this<HttpMgr> {
    Q_OBJECT
public:
    ~HttpMgr();

    // 实现 HttpMgr 特有的行为
    void PostHttpReq(QUrl url, QJsonObject json, ReqId req_id, Modules mod);

private:
    // 使 HttpMgr 的构造函数成为私有,以实现单例模式
    friend class SingletonBase<HttpMgr>;
    HttpMgr();

    // 网络请求管理器
    QNetworkAccessManager _manager;

private slots:
    // 完成 HTTP 请求后的回调函数
    void slot_http_finish(ReqId id, QString res, ErrorCodes err, Modules mod);

signals:
    // HTTP 请求完成后的信号
    void sig_http_finish(ReqId id, QString res, ErrorCodes err, Modules mod);
    // 注册模块完成后的信号
    void sig_reg_mod_finish(ReqId id, QString res, ErrorCodes err);
};

#endif // HTTPMGR_H

 将 单例模式 和 CRTP 结合起来,消除了虚拟函数和运行时多态的开销,改为在编译时使用 CRTP 实现静态多态性。现在,HttpMgr 类通过继承自 SingletonBase<HttpMgr> 来实现单例模式,并且通过 GetInstance() 获取单例实例。SingletonBase 提供了静态多态性,而 HttpMgr 只需专注于实现具体的业务逻辑。

;