其实是之前在学院群里的一个问题,当时并没有真正解决,只是通过暴力解决了。一个是对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,奇异递归模板模式)的核心机制。让我们深入探讨一下:
- CRTP 的基本结构
在 CRTP 中,基类模板接受派生类作为模板参数:
template <typename DerivedT>class Base { // ...};
class Derived : public Base<Derived> { // ...};- 编译器的两阶段名称查找
CRTP 能够工作的关键在于 C++ 编译器的两阶段名称查找(two-phase name lookup)机制:
a. 第一阶段:在模板定义时,编译器会检查语法和非依赖名称。 b. 第二阶段:在模板实例化时,编译器会检查依赖名称。
- 延迟绑定
当基类模板中使用 Derived 类的成员时,这些名称被视为依赖名称。编译器会延迟这些名称的解析,直到模板实例化时。
- 示例解析
考虑以下代码:
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()方法。
- 为什么不报错
编译器不会立即报错,因为:
- 在模板定义时,编译器不知道
Derived的具体内容,所以它假设Derived会提供所需的方法。 - 只有在实际实例化模板时,如果缺少所需的方法,编译器才会报错。
- 注意事项
- 如果
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 > > -> byteVectorget_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就是被CRTP的Derived类的模板参数。
关于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;}