c++的"Curiously Recurring Template Pattern" (CRTP) 有哪些高级用法? (静态多态进阶)

CRTP通过编译期绑定调用路径实现零开销接口模拟,基类模板直接static_cast调用派生类方法,无虚函数、无vtable;配合static_assert可约束接口契约,支持静态访问控制与编译期能力探测,

但不适用于运行时多态场景。

CRTP 如何实现零开销的接口模拟?

CRTP 的核心价值不是“替代虚函数”,而是让编译器在编译期就确定调用路径,彻底消除 vtable 查找和间接跳转。典型做法是基类模板中直接调用 static_cast(this)->method(),而非声明 virtual 函数。

  • 基类不带虚析构函数,也不含任何虚函数 —— 这是零开销的前提;若需安全 delete 派生对象,必须显式提供非虚析构 + 静态断言约束
  • 派生类必须继承自 Base,且不能中途再被其他模板包装(否则 static_cast 会失败)
  • 常见误用:在基类中调用未在派生类定义的函数,编译错误信息往往冗长,建议配合 static_assert 检查接口契约,例如:
    static_assert(std::is_same_v().do_work()), void>,
                    void>, "Derived must implement do_work()");

如何用 CRTP 实现静态访问控制(如 only-own-type 操作)?

利用模板参数 Derived 的唯一性,可在基类中限制某些操作仅对“本派生类自身”有效,比如禁止跨类型赋值、只允许同类型比较。

  • 在基类中定义 operator== 时,参数类型写死为 const Derived&,而非泛化的 const Base& —— 这样 A{} == B{} 直接不匹配重载,无需运行时判断
  • 配合 friend 和私有构造,可实现“仅本类型可构造”的工厂模式变体,例如:
    template
    class NonCopyable {
    protected:
        NonCopyable() = default;
        ~NonCopyable() = default;
    public:
        NonCopyable(const NonCopyable&) = delete;
        NonCopyable& operator=(const NonCopyable&) = delete;
        friend T; // only T can access protected ctor/dtor
    };
  • 注意:C++20 起可改用 requires std::same_as 替代部分 static_cast 断言,但 CRTP 主体逻辑仍需保持模板参数显式传递

CRTP 与 SFINAE / Concepts 结合做编译期能力探测

CRTP 基类可以成为“能力分发中心”:根据派生类是否提供某个嵌套类型、成员函数或 constexpr 值,启用不同行为分支。

  • 典型场景:统一序列化接口,但对支持 to_json() 的类型走 JSON 路径,对支持 serialize(Writer&) 的走二进制路径 —— 所有决策在编译期完成
  • 推荐写法:用 decltype + void_t 或 C++20 requires 构建 trait,再通过 if constexpr 分支,避免宏或特化污染
  • 陷阱:不要在基类模板中直接依赖派生类未实例化的成员(如未定义的 static constexpr),否则会导致 ODR-violation 或隐式实例化失败;应始终用 SFINAE 友好方式探测

为什么 CRTP 不适合替代所有多态场景?

CRTP 是静态多态,它无法处理运行时才确定类型的集合,比如 std::vector<:unique_ptr>> 这种异构容器。

  • 若强行用 std::vector<:unique_ptr>>>,每个 Concrete 对应独立的基类特化,无法共用同一容器类型
  • 混合使用 CRTP 和动态多态(如基类含虚析构+CRTP 辅助功能)极易引发对象切片或 static_cast 失败,除非严格约定内存布局一致且无虚函数干扰
  • 调试困难:编译错误常出现在实例化深度较深的模板栈中,IDE 很难跳转到真正缺失的派生类成员定义处

真正需要运行时类型擦除的地方,别硬套 CRTP;它最锋利的用途,是把“本该在运行时做的类型判断”,压缩进编译期的一个模板参数里 —— 这个参数一旦写错,整个链条就断了,没有妥协余地。