【C++】奇异递归模板模式(CRTP)
1.引言
奇异递归模板模式(Curiously Recurring Template Pattern, CRTP)是C++模板编程的一种习惯用法,即类X继承自类模板Y,并以X自身为模板参数。例如:
1
2
3
4
template<class T>
class Y {};
class X : public Y<X> {};
这种技术最早于1989年作为“F-界量化”(F-bounded quantification)被提出,Jim Coplien于1995年称之为 “CRTP” 。
2.用例
CRTP的用途主要有两类:实现编译期多态以及向派生类添加功能。
2.1 编译期多态
C++中的多态是基于虚函数表实现的,需要在运行时将虚函数调用分派到实际的派生类函数。CRTP可用于实现“编译期多态”(也叫“静态多态”)。考虑下面的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
template<class T>
class Vehicle {
public:
int num_wheels() { return static_cast<T*>(this)->num_wheels_impl(); }
protected:
Vehicle() = default; // creation of Vehicle objects is UB
};
class Bicycle : public Vehicle<Bicycle> {
public:
int num_wheels_impl() { return 2; }
};
class Car : public Vehicle<Car> {
public:
int num_wheels_impl() { return 4; }
};
int main() {
Bicycle b;
std::cout << "Bicycles have " << b.num_wheels() << " wheels\n";
Car c;
std::cout << "Cars have " << c.num_wheels() << " wheels\n";
return 0;
}
输出结果如下:
1
2
Bicycles have 2 wheels
Cars have 4 wheels
在基类函数Vehicle::num_wheels()中,通过static_cast将this转换为模板参数T(同时也是派生类)的指针,并调用派生类函数num_wheels_impl()。这样实现了类似多态的效果,同时避免了虚函数调用的开销,因为所有调用都在编译期确定(性能对比参见The cost of dynamic (virtual calls) vs. static (CRTP) dispatch in C++)。这种模式广泛用于Windows的ATL和WTL等库中。
注: 这段代码之所以能通过编译,是因为类模板的成员函数体只有在实际使用时才会被实例化。
- 当编译器遇到
Bicycle类的定义时,会实例化Vehicle<Bicycle>,但并不会立即实例化num_wheels()的函数体,因为此时Bicycle还不是完整类型,无法获得num_wheels_impl()函数的地址。 - 在调用
b.num_wheels()时,Bicycle已经是完整类型,Vehicle<Bicycle>::num_wheels()函数体被实例化为return static_cast<Bicycle*>(this)->num_wheels_impl(),即调用Bicycle::num_wheels_impl(),此时该函数的定义是已知的。
Java中也有类似的用法,例如
1
2
3
4
5
6
7
8
public class Item implements Comparable<Item> {
private String name;
@Override
public int compareTo(Item other) {
return name.compareTo(other.name);
}
}
但由于Java对泛型类的处理方式与C++模板不同,因此无法实现编译期多态。
deducing this
使用C++23的deducing this特性可以简化CRTP的写法。
如果基类函数num_wheels()使用了显式对象参数(explicit object parameter),基类Vehicle就不必是模板,因为self参数可以自动推导为正确的派生类型,而无需使用static_cast。
注:GCC 14以上版本才支持该特性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Vehicle {
public:
template<class Self>
int num_wheels(this Self&& self) { return self.num_wheels_impl(); }
// or
// int num_wheels(this auto&& self) {...}
protected:
Vehicle() = default; // creation of Vehicle objects is UB
};
class Bicycle : public Vehicle {
public:
int num_wheels_impl() { return 2; }
};
class Car : public Vehicle {
public:
int num_wheels_impl() { return 4; }
};
https://godbolt.org/z/vaYhaKra9
2.2 添加功能
CRTP的另一个用途是向派生类添加功能。在上一节的例子中可以看到,基类可以利用模板参数和对this类型转换来访问派生类的函数。利用这一点,基类可以提供一些通用功能,可以被多个派生类复用(这类似于Python的混入类)。
在下面的例子中,利用CRTP向派生类添加数值计算函数(来自Jonathan Boccara)。假设Sensitivity有一个值:
1
2
3
4
5
6
7
8
9
10
11
class Sensitivity {
public:
explicit Sensitivity(double v) :value_(v) {}
double value() const { return value_; }
void set_value(double value) { value_ = value; }
// rest of the sensitivity's rich interface...
private:
double value_;
};
现在希望添加一些数值计算函数,例如缩放(乘以一个常数)、平方、相反数等。为了让其他类也能复用这些操作,将这三个函数放在一个基类NumericalFunctions中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<class T>
class NumericalFunctions {
public:
void scale(double multiplicator) {
T& self = static_cast<T&>(*this);
self.set_value(self.value() * multiplicator);
}
void square() {
T& self = static_cast<T&>(*this);
self.set_value(self.value() * self.value());
}
void opposite() {
scale(-1.0);
}
};
class Sensitivity : public NumericalFunctions<Sensitivity> {
// ...
};
NumericalFunctions使用了派生类的value()和set_value()函数完成数值计算。任何定义了这两个函数的类X都可以通过继承NumericalFunctions<X>获得这些数值计算函数。如果不使用CRTP,则需要将value()和set_value()放在基类中,并声明为纯虚函数,由派生类覆盖这两个函数。
在这个CRTP例子中,“继承”的含义与其他情况不同。通常,
- 派生类“是一个”基类。
- 基类提供接口,派生类提供实现。
- 在泛型代码中通过基类调用虚函数,由多态动态分派到派生类函数。
对于CRTP的这种用法,情况则完全不同:
- 派生类不“是一个”基类,而是通过继承基类来扩展接口,以添加更多功能。
- 派生类向基类提供接口,基类使用派生类函数(如
value()和set_value())。 - 直接使用派生类,而永远不会使用基类。
因此,CRTP有时也称为倒置继承(upside-down inheritance)。
2.3 对象计数器
下面是另一个添加功能的例子。类模板Counter可以统计一个类的对象创建和销毁的数据,这可以很容易地使用CRTP实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <vector>
template<class T>
class Counter {
public:
static int objects_created;
static int objects_alive;
Counter() {
++objects_created;
++objects_alive;
}
Counter(const Counter&) {
++objects_created;
++objects_alive;
}
protected:
~Counter() { // objects should never be removed through pointers of this type
--objects_alive;
}
};
template<class T> int Counter<T>::objects_created = 0;
template<class T> int Counter<T>::objects_alive = 0;
class X : public Counter<X> {};
class Y : public Counter<Y> {};
int main() {
X xs[10];
std::vector<Y> ys(20);
ys.resize(15);
std::cout << "X: " << X::objects_created << " objects created, " << X::objects_alive << " alive\n";
std::cout << "Y: " << Y::objects_created << " objects created, " << Y::objects_alive << " alive\n";
return 0;
}
输出结果如下:
1
2
X: 10 objects created, 10 alive
Y: 20 objects created, 15 alive
每次创建类X的对象时,Counter<X>的构造函数会将创建计数和存活计数各加1;每次销毁类X的对象时,Counter<X>的析构函数会将存活计数减1。Counter<X>和Counter<Y>是两个独立的类,因此能分别对X和Y进行计数。在这个例子中,这种类的区别是模板参数T的唯一用途,这也是不能使用非模板基类的原因。
2.4 std::ranges::view_interface
标准库中使用CRTP的一个例子是头文件<ranges>中的类模板std::ranges::view_interface,用于辅助自定义视图类(关于“视图”的概念详见《C++20范围库》)。
视图类只需使用CRTP继承std::ranges::view_interface,并提供返回开始和结束迭代器的成员函数begin()和end()即可。该模板会根据迭代器类别提供其他成员函数,如size()、empty()、operator[]等。例如:
1
2
3
4
5
6
class my_view : public std::ranges::view_interface<my_view> {
public:
auto begin() const { /*...*/ }
auto end() const { /*...*/ }
// size() is provided if begin() returns a forward iterator and end() returns a sentinel for it.
};
其他成员函数均基于begin()和end()实现,这也是向派生类添加功能的一个例子。例如,成员函数size()定义为end() - begin()。
1
2
3
4
5
6
7
8
9
10
11
12
template<class D>
requires is_class_v<D> && same_as<D, std::remove_cv_t<D>>
class view_interface {
constexpr D& derived() { return static_cast<D&>(*this); }
public:
constexpr auto size()
requires forward_range<D> && sized_sentinel_for<sentinel_t<D>, iterator_t<D>>
{ return ranges::end(derived()) - ranges::begin(derived()); }
// ...
};
其中ranges::begin(t)会调用t.begin(),ranges::end(t)会调用t.end()。
完整实现参见主流编译器的标准库源码:
- GCC: libstdc++/ranges_util.h
- Clang: libc++/view_interface.h
- MSVC: STL/xutility
2.5 std::enable_shared_from_this
标准库中另一个CRTP的例子是头文件<memory>中的类模板std::enable_shared_from_this。继承std::enable_shared_from_this<T>会为类T提供成员函数shared_from_this(),该函数返回一个新的std::shared_ptr<T>,能够正确地与已有的管理当前对象的智能指针共享所有权。
智能指针std::shared_ptr能够共享一个对象的所有权,并在引用计数降为0时自动销毁对象。这一机制能正确工作的前提是管理同一个对象的所有std::shared_ptr必须共享引用计数,否则会导致对象被多次销毁(未定义行为,可能导致程序崩溃)。
假设一个类T的对象当前已经由一个名为pt的std::shared_ptr管理。为了获得与pt共享所有权的其他std::shared_ptr,应该直接拷贝pt:
1
2
std::shared_ptr<T> pt(new T);
auto pt2 = pt;
但是,在类T内部无法访问pt。要在成员函数中创建一个管理当前对象的std::shared_ptr,直接返回std::shared_ptr<T>(this)是错误的。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <memory>
class Bad {
public:
~Bad() { std::cout << "Bad::~Bad()\n"; }
std::shared_ptr<Bad> getptr() {
return std::shared_ptr<Bad>(this);
}
};
struct X {
std::shared_ptr<Bad> p;
explicit X(Bad* b) :p(b->getptr()) {}
};
void test_bad() {
// Bad, each shared_ptr thinks it is the only owner of the object
Bad* b = new Bad;
std::shared_ptr<Bad> p(b);
X x(b);
std::cout << "p.use_count() = " << p.use_count() << '\n';
std::cout << "x.p.use_count() = " << x.p.use_count() << '\n';
} // UB: double-delete of Bad
int main() {
test_bad();
return 0;
}
GCC编译器得到的结果如下:
https://godbolt.org/z/fhcPa33Td
1
2
3
4
5
6
p.use_count() = 1
x.p.use_count() = 1
Bad::~Bad()
Bad::~Bad()
free(): double free detected in tcache 2
Program terminated with signal: SIGSEGV
这里的问题在于p和x.p管理了同一个Bad对象,但它们的引用计数是独立的,在函数退出时会各调用一次Bad的析构函数,导致重复销毁(如果Bad的析构函数包含delete语句就会引发coredump)。
正确的做法是继承std::enable_shared_from_this(使用CRTP),并调用shared_from_this()来创建std::shared_ptr。需要注意的是,只能对已经由std::shared_ptr管理的对象调用该函数,否则会抛出异常std::bad_weak_ptr。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <memory>
class Good : public std::enable_shared_from_this<Good> {
public:
std::shared_ptr<Good> getptr() {
return shared_from_this();
}
};
struct X {
std::shared_ptr<Good> p;
explicit X(Good* g) :p(g->getptr()) {}
};
void test_good() {
// Good: the two shared_ptr's share the same object
Good* g = new Good;
std::shared_ptr<Good> p(g);
X x(g);
std::cout << "p.use_count() = " << p.use_count() << '\n';
std::cout << "x.p.use_count() = " << x.p.use_count() << '\n';
}
void misuse_good() {
// Bad: shared_from_this is called without having std::shared_ptr owning the caller
try {
Good g;
std::shared_ptr<Good> p = g.getptr();
}
catch (std::bad_weak_ptr& e) {
std::cout << e.what() << '\n';
}
}
int main() {
test_good();
misuse_good();
return 0;
}
输出结果如下:
https://godbolt.org/z/4hTPWv4vT
1
2
3
p.use_count() = 2
x.p.use_count() = 2
bad_weak_ptr
- 在
test_good()中,x.p是通过Good::shared_from_this()得到的,与已有的p共享引用计数,因此是正确的。 - 在
misuse_good()中,对象g是在栈上创建的,调用shared_from_this()时还没有任何std::shared_ptr管理该对象,因此会抛出std::bad_weak_ptr。
实现原理
类模板std::enable_shared_from_this<T>有一个std::weak_ptr<T>类型的成员weak_this,用于记录管理当前对象的智能指针(std::weak_ptr类似于std::shared_ptr,但并不拥有对象的所有权,即不影响引用计数,如果其指向的对象被销毁,该指针就会“过期”)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<class T>
class enable_shared_from_this {
protected:
constexpr enable_shared_from_this() {}
constexpr enable_shared_from_this(const enable_shared_from_this& other) {}
~enable_shared_from_this() {}
enable_shared_from_this& operator=(const enable_shared_from_this& rhs) { return *this; }
public:
shared_ptr<T> shared_from_this() { return shared_ptr<T>(weak_this); }
shared_ptr<const T> shared_from_this() const { return shared_ptr<const T>(weak_this); }
friend class shared_ptr<T>;
private:
mutable weak_ptr<T> weak_this;
};
可以看到,std::enable_shared_from_this类本身非常简单:
- 在构造函数中,将
weak_this默认初始化为空指针。 - 函数
shared_from_this()直接返回由weak_this构造的std::shared_ptr。如果weak_this仍是空指针,则抛出异常std::bad_weak_ptr。
成员weak_this在类内并没有被赋过值,而是应该在首次创建管理当前对象的std::shared_ptr时,将其赋给该对象的weak_this成员。为了实现这一点,需要在std::shared_ptr的构造函数中判断被管理的对象是否继承了std::enable_shared_from_this以及它的weak_this成员是否为空指针,如果是则使用自身对其赋值(这也是std::enable_shared_from_this将std::shared_ptr声明为友元的原因)。一种简化的实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
template<class T>
class shared_ptr {
public:
explicit shared_ptr(T* ptr) {
// ...
if constexpr (std::is_base_of_v<enable_shared_from_this<T>, T>) {
if (ptr && ptr->weak_this.expired())
ptr->weak_this = *this;
}
}
// ...
};
注意,实际的标准库实现可能使用指针转换(即T*能否转换为std::enable_shared_from_this<T>*)来判断继承关系,这种方式只能识别public继承(见《C++程序设计原理与实践》笔记 第14章 14.3.4节)。因此在继承std::enable_shared_from_this时必须使用public继承,否则可能会导致weak_this无法被正确赋值。
参见主流编译器的标准库源码:
- GCC: libstdc++/shared_ptr_base.h
std::__shared_ptr::_M_enable_shared_from_this_with() - Clang: libc++/shared_ptr.h
std::shared_ptr::__enable_weak_this() - MSVC: STL/memory
std::shared_ptr::Set_ptr_rep_and_enable_shared()
3.陷阱
在使用CRTP时可能会遇到一些陷阱。
(1)继承CRTP基类时模板参数错误,可能导致未定义行为。例如:
1
2
3
4
5
template<class T> class Base { ... };
class Derived1 : public Base<Derived1> { ... };
class Derived2 : public Base<Derived1> { ... }; // wrong template argument
为了避免这个问题,可以在基类中添加一个私有构造函数,并将模板参数T声明为友元:
1
2
3
4
5
6
7
template<class T>
class Base {
// ...
private:
Base() {}
friend T;
};
派生类的构造函数一定会调用基类的构造函数。Base的构造函数是私有的,只有友元类可以访问,而唯一的友元类是模板参数T。如果派生类和模板参数不同,代码将编译失败。
(2)由于CRTP基类的函数不是虚函数,派生类中的函数会隐藏基类中的同名函数。不过,可以利用这一特性实现编译期多态的“覆盖”。例如:
1
2
3
4
5
6
7
8
9
10
template<class T>
class Base {
public:
void fun();
};
class Derived : public Base<Derived> {
public:
void fun(); // this hides Base::fun() !
};
(3)避免在CRTP基类实例化时检测派生类成员。
考虑下面的例子。Base类的成员函数fun()有两个重载,如果派生类定义了fun_impl()则使用派生类的实现,否则使用基类的默认实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <type_traits>
template<class T, class = void>
struct has_fun_impl : public std::false_type {};
template<class T>
struct has_fun_impl<T, std::void_t<decltype(std::declval<T>().fun_impl())>> : public std::true_type {};
template<class D>
class Base {
public:
void test_fun_impl() { std::cout << std::boolalpha << has_fun_impl<D>::value << '\n'; }
// (1)
template<class T = void, std::enable_if_t<!has_fun_impl<D>::value, T>* = nullptr>
void fun() { std::cout << "Base::fun()\n"; }
// (2)
template<class T = void, std::enable_if_t<has_fun_impl<D>::value, T>* = nullptr>
void fun() { static_cast<D*>(this)->fun_impl(); }
};
class Derived1 : public Base<Derived1> {
public:
void fun_impl() { std::cout << "Derived1::fun_impl()\n"; }
};
class Derived2 : public Base<Derived2> {};
int main() {
Derived1 d1;
d1.test_fun_impl();
d1.fun();
Derived2 d2;
d2.test_fun_impl();
d2.fun();
return 0;
}
期望d1.fun()会调用Derived1::fun_impl(),d2.fun()会调用Base::fun()的重载(1)。但实际上,二者都调用了Base::fun()的重载(1)。
https://godbolt.org/z/vj3c1KEd1
1
2
3
4
false
Base::fun()
false
Base::fun()
更奇怪的是,如果将基类中的两个fun()删除,test_fun_impl()函数就能输出正确的结果。
https://godbolt.org/z/s5Wdavf9n
1
2
true
false
导致这一问题的原因仍然与2.1节中解释的模板实例化机制有关。
- 当编译器遇到
Derived1类时,会实例化Base<Derived1>,而此时Derived1还不是完整类型。 - 编译器遇到函数
Base::fun(),虽然不会实例化函数体,但会检查模板参数中的类型,因此需要实例化has_fun_impl<Derived1>。 - 此时编译器并不知道
Derived1类有成员函数fun_impl(),因此has_fun_impl<Derived1>::value为false,导致重载(2)的模板参数非良构。 - 由于SFINAE规则,调用
d1.fun()会选择重载(1)。 - 如果删除了两个
fun(),则直到调用d1.test_fun_impl()时才会实例化has_fun_impl<Derived1>。此时Derived1已经是完整类型,编译器知道它有fun_impl()函数,因此has_fun_impl<Derived1>::value为true。
解决该问题的一种方式是在派生类中隐藏基类的同名函数,如前面的(2)所述。
1
2
3
4
5
6
7
8
9
10
template<class D>
class Base {
public:
void fun() { std::cout << "Base::fun()\n"; }
};
class Derived1 : public Base<Derived1> {
public:
void fun() { std::cout << "Derived1::fun()\n"; }
};
https://godbolt.org/z/Tj7TWo7E1
第二种方式是修改fun()的模板参数,让std::enable_if_t检测has_fun_impl<T>而不是has_fun_impl<D>,从而将检测逻辑推迟到调用fun()时。(另见《C++20之概念》 3.3节)
1
2
3
4
5
6
7
// (1)
template<class T = D, std::enable_if_t<!has_fun_impl<T>::value, T>* = nullptr>
void fun() { std::cout << "Base::fun()\n"; }
// (2)
template<class T = D, std::enable_if_t<has_fun_impl<T>::value, T>* = nullptr>
void fun() { static_cast<D*>(this)->fun_impl(); }
https://godbolt.org/z/Yvsjc9nhh
第三种方式是使用constexpr if语句,将检测代码放在成员函数内部(本质上仍然是推迟检测逻辑)。
1
2
3
4
5
6
7
void fun() {
if constexpr (has_fun_impl<D>::value) {
static_cast<D*>(this)->fun_impl();
} else {
std::cout << "Base::fun()\n";
}
}
https://godbolt.org/z/7nj1GzMnG
参考
- https://en.cppreference.com/w/cpp/language/crtp
- https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
- https://www.fluentcpp.com/2017/05/12/curiously-recurring-template-pattern/
- https://www.sandordargo.com/blog/2019/03/13/the-curiously-recurring-templatep-pattern-CRTP
- https://www.cnblogs.com/yang-wen/p/8573269.html
- https://www.cnblogs.com/RioTian/p/17881026.html