Post

【C++】奇异递归模板模式(CRTP)

【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_castthis转换为模板参数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>是两个独立的类,因此能分别对XY进行计数。在这个例子中,这种类的区别是模板参数T的唯一用途,这也是不能使用非模板基类的原因。

2.4 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的对象当前已经由一个名为ptstd::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

这里的问题在于px.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默认初始化为空指针。
  • 在首次创建管理当前对象的std::shared_ptr时,将其赋给该对象的weak_this成员。
  • 函数shared_from_this()直接返回由weak_this构造的std::shared_ptr。如果weak_this仍是空指针,则抛出异常std::bad_weak_ptr

主要的难点在于std::shared_ptr的构造函数需要判断被管理的对象是否继承了std::enable_shared_from_this以及它的weak_this成员是否为空指针,如果是则使用自身对其赋值。一种简化的实现如下:

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无法被正确赋值。

主流编译器的标准库源码:

3.陷阱

在使用CRTP时可能会遇到一些陷阱。

1.继承CRTP基类时模板参数错误,可能导致未定义行为。

1
2
3
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 do_something();
};

class Derived : public Base<Derived> {
public:
    void do_something();  // this hides Base::do_something() !
};

参考

This post is licensed under CC BY 4.0 by the author.