C++ CRTP中基类接口获取子类定义的类型?

其实是之前在学院群里的一个问题,当时并没有真正解决,只是通过暴力解决了。一个是对CRTP的理解不够深入,另一个是对于C++的模板元编程 的使用不够熟练。希望记录一下。

问题

下面代码是很常见的CRTP,我们的目的是在Base类中获取Derived类中定义的RawT类型。 但是却报错了no typename RawT in Derived<byteVector>,因为Base类并不知道Derived类的定义,所以无法获取RawT类型。 可是CRTP却让Base类能够使用到Derived类的成员函数?

#include <print>
#include <vector>

using byteVector = std::vector< std::byte >;

template < typename DerivedT >
class Base {
  public:
    using RawT = typename DerivedT::RawT;

    void foo() {
        std::println("{}", typeid(RawT).name());
        static_cast< DerivedT* >(this)->impl();
    }
};

template < typename T >
class Derived;

template <>
class Derived< byteVector > : public Base< Derived< byteVector > > {
    template < typename U >
    friend class Base; // make all Base's member functions visible to Derived

    using RawT = byteVector;

    void impl() { std::println("Derived<int>::impl()"); }
};

int main() {
    Derived< byteVector > d;
    d.foo();
    return 0;
}

为什么CRTP能够让Base类使用到Derived类的成员函数?

以下来自Claude 3.5 Sonnet,感觉解释的很好,我就直接翻译过来了。

这是一个很好的问题,涉及到 CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)的核心机制。让我们深入探讨一下:

  1. CRTP 的基本结构

在 CRTP 中,基类模板接受派生类作为模板参数:

template <typename DerivedT>
class Base {
    // ...
};

class Derived : public Base<Derived> {
    // ...
};
  1. 编译器的两阶段名称查找

CRTP 能够工作的关键在于 C++ 编译器的两阶段名称查找(two-phase name lookup)机制:

a. 第一阶段:在模板定义时,编译器会检查语法和非依赖名称。 b. 第二阶段:在模板实例化时,编译器会检查依赖名称。

  1. 延迟绑定

当基类模板中使用 Derived 类的成员时,这些名称被视为依赖名称。编译器会延迟这些名称的解析,直到模板实例化时。

  1. 示例解析

考虑以下代码:

template <typename DerivedT>
class Base {
public:
    void interface() {
        static_cast<DerivedT*>(this)->implementation();
    }
};

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

在这个例子中:

  • 当编译器首次遇到 Base 类模板时,它不会报错说找不到 implementation()
  • 编译器知道 Derived 是一个模板参数,因此假设 implementation() 将在 Derived 中定义。
  • Derived 类实例化 Base<Derived> 时,编译器检查 Derived 是否真的有 implementation() 方法。
  1. 为什么不报错

编译器不会立即报错,因为:

  • 在模板定义时,编译器不知道 Derived 的具体内容,所以它假设 Derived 会提供所需的方法。
  • 只有在实际实例化模板时,如果缺少所需的方法,编译器才会报错。
  1. 注意事项
  • 如果 Derived 类没有定义所需的方法,错误只会在使用 Derived 类时出现,而不是在定义 Base 模板时。
  • 这种延迟检查可能导致错误消息出现在意想不到的地方,有时会使调试变得复杂。

总结:CRTP 利用了 C++ 模板的延迟实例化和两阶段名称查找机制,允许基类模板使用尚未定义的派生类成员,而不会在模板定义时报错。这种机制提供了强大的静态多态性,但也需要开发者确保正确实现所有必要的方法。

为什么Base类无法获取Derived类中定义的RawT类型?

严格编译期确定

这与CRTP是有不同的,在继承时发生了模板实例化public Base<Derived<byteVector>>, 此时实例化的Base类中 using RawT = typename Der::RawT;会去查找Derived<byteVector>类中的RawT类型,但是请注意,此时的 Derived<byteVector>类还没有定义RawT类型(因为我们从继承的时候就开始了Base的实例化,而此时甚至还没有进入到Derived的定义) 所以会报错。也就是说,Derived<byteVector>的完整类型出现在Base之后。

编译期间Derived<byteVector>在类型不完整的时候就被依赖了。

CRTP的延迟绑定

CRTP中,基类通常不会直接使用派生类的成员,而是使用其成员函数。方法的调用完全可以延迟到实例化完成后, 而类型的获取则必须在编译时确定。

解决方案

获取一个模板类的第一个参数

其实通过分析我们不难发现,我们不能在实例化的时候用Base的模板参数来获取RawT,因为这样的话派生类必然还没有完整定义。 所以我们可以通过套一层模板来直接获取一个模板类的参数,简单来说就是有一个模板能获取模板类的模板参数。 这样说比较绕,我们直接看代码。

get_raw_t< Derived< byteVector > > -> byteVector
get_raw_t< std::vector< int > > -> int

所以目标是构造一个get_raw_t模板,能够获取Derived类的RawT类型。

template < typename >
struct get_raw;

/*
 * @brief: get_raw< std::vector< std::byte > > -> std::byte
 */
template < template < typename... > class TemplateClass, typename InnerType >
struct get_raw< TemplateClass< InnerType > > {
    using type = InnerType;
};

template < typename T >
using get_raw_t = typename get_raw< T >::type;

我们知道,类似于std::vector< std::byte >这样的模板类,其实是一个被实例化的模板类。 如果使用get_raw< std::vector< std::byte > >,那么就会匹配到特化的get_raw,从而获取到std::byte类型。 也即是get_raw< TemplateClass< InnerType > >对应get_raw< std::vector< std::byte > >。 所以 < TemplateClass, InnerType > 匹配到< std::vector, std::byte >

改进我们的代码

只需要在Base类中使用get_raw_t就可以了。

template < typename DerivedT >
class Base {
  public:
    // bypass using RawT = typename Der::RawT;
    using RawT = get_raw_t< DerivedT >;

    void foo() {
        std::println("{}", typeid(RawT).name());
        static_cast< DerivedT* >(this)->impl();
    }
};

本质上还是绕过DerivedT这个不完整类型。通过实例化的get_raw_t来获取RawT类型。当然,必须保证Derived类中的 RawT就是被CRTPDerived类的模板参数。

关于C++ 23之后CRTP的一些改善

C++ 23引入了显示this。大大增强了CRTP的能力,可以直接在Base类中获取Derived类的类型。

template < typename Der >
class Base {
  public:
    using RawT = get_raw_t< Der >;

    void foo(this auto&& self) {
        std::println("{}", typeid(RawT).name());
        self.impl();
    }
};

通过将this作为参数传递给foo函数,我们实例化的Derived就可以被直接传入作为第一个参数, 从而直接调用Derived的成员函数。不过此时应该将Base设置为友元类,以便Base类能够访问Derived类的私有成员。

template < typename T >
class Derived;

template <>
class Derived< byteVector > : public Base< Derived< byteVector > > {
    template < typename U >
    friend class Base; // make all Base's member functions visible to Derived

    void impl() { std::println("Derived<int>::impl()"); }
};

int main() {
    Derived< byteVector > d;
    d.foo();
    return 0;
}