奇妙递归模板模式和混入技巧
奇妙递归模板模式#
奇妙递归模板模式(curiously recurring template pattern)指的是将派生类作为模板实参传递给自己的某个基类。
template<typename Derived>
class CuriousBase { };
class Curious : public CuriousBase<Curious> { };
允许派生类是个模板类。
template<typename T>
class Curious : public CuriousBase<Curious<T>> { };
使用这个技巧的一个好处是,可以把一些重复的公共逻辑放到基类中去执行,比如一个统计当前对象数量的方法:
template<typename Derived>
class BaseCount {
public:
BaseCount() { ++count; }
virtual ~BaseCount() { --count; }
BaseCount(const BaseCount&) { ++count; }
BaseCount(BaseCount&&) { ++count; }
public:
static int live() { return count; }
private:
inline static int count = 0;
};
class Object : public BaseCount<Object> { };
int main() {
std::vector<Object> objs(10, Object());
cout << "current object size: " << Object::live() << endl; // 10
objs.pop_back();
cout << "current object size: " << Object::live() << endl; // 9
return 0;
}
运算符实现#
运算符的重载往往是成组出现的,重载了运算符的类,一般也会重载!=运算符,并且可以委托给运算符来实现。
bool operator!=(const X& x1, const X& x2) {
return !(x1 == x2);
}
于是,在大量使用到运算符重载时,自然就会考虑将运算符重载写成模板的形式。
template<typename T>
bool operator!=(const T& x1, const T& x2) {
return !(x1 == x2);
}
这在标准库的<utility>
中可以找到,但是被降级到了std::rel_ops
命名空间下,主要是因为模板的匹配问题,可能会导致匹配到非预期的方法。
在这里就可以使用CRTP的技巧来使得自定义的重载运算符比一般化的重载具有更高的优先级,既提高了代码的复用性的同时,又避免了运算符过于一般化的弊端。
template<typename Derived>
class CuriousNotEqual {
public:
friend bool operator!=(const Derived& x1, const Derived& x2) {
return !(x1 == x2);
}
};
template<typename T>
class Equal : public CuriousNotEqual<Equal<T>> {
public:
friend bool operator==(const Equal<T>& x1, const Equal<T>& x2) {
// ...
}
};
将运算符重载成友元是一个比较常用的操作,如果定义为成员函数,就会存在第一个隐藏入参this指针带来的转换问题。
奇妙递归模板模式使得一些通用的行为被纳入到了基类中,派生类中不用再重写一遍,同时,又保留了最终派生类的身份。
结合Barton-Nackman技巧——使用友元重载指定模板实参的模板类的运算符函数,可以为类的实例自定义通用的运算符重载,而不会对外界产生副作用。
门面模式#
使用CRTP,让基类使用派生类中的相关函数来实现类的某些或全部的公共接口,这样可以使派生类尽可能地提供更小和更易实现的公共接口,这种模式称为门面(facade)模式。
例如,为每个自定义的类实现各种迭代器是个工作量很大的任务,而且会很无聊,于是我们可以定义一个迭代器的门面类(只展示了部分接口的实现):
template<typename Derived, typename Value, typename Category>
class IteratorFacade {
public:
using value_type = typename std::remove_const_t<Value>;
using reference = Value&;
using pointer = Value*;
using difference_type = std::ptrdiff_t;
using iterator_category = Category;
// 输入迭代器接口
reference operator*() const {
return asDerived().dereference();
}
pointer operator->() const {
// ...
}
Derived& operator++() {
asDerived().increment();
return asDerived();
}
Derived operator++(int) {
Derived result(asDerived());
asDerived().increment();
return result;
}
friend bool operator==(const IteratorFacade& lhs, const IteratorFacade& rhs) {
return lhs.asDerived().equal(rhs.asDerived());
}
// 双向迭代器
Derived& operator--() {
// ...
}
Derived operator--(int) {
// ...
}
// 随机访问迭代器
reference operator[](difference_type n) const {
// ...
}
Derived& operator +=(difference_type n) {
// ...
}
friend difference_type operator-(const IteratorFacade& lhs, const IteratorFacade& rhs) {
// ...
}
friend bool operator<(const IteratorFacade& lhs, const IteratorFacade& rhs) {
// ...
}
private:
Derived& asDerived() {
return *static_cast<Derived*>(this);
}
const Derived& asDerived() const {
return *static_cast<const Derived*>(this);
}
};
门面类通过私有成员函数asDerived
来将当前的this指针转换成所需的派生类对象。输入迭代器使用派生类的dereference
获取对象引用,使用increment
指向下个元素。
template<typename T>
struct ListNode {
T value;
ListNode<T>* next = nullptr;
~ListNode() {
delete next;
}
};
template<typename T>
class ListNodeIterator : public IteratorFacade<ListNodeIterator<T>, T, std::forward_iterator_tag> {
public:
ListNodeIterator(ListNode<T>* current = nullptr) : m_cur(current) { }
public:
T& dereference() const {
return m_cur->value();
}
void increment() {
m_cur = m_cur->next;
}
bool equal(const ListNodeIterator& other) const {
return m_cur == other.m_cur;
}
private:
ListNode<T>* m_cur = nullptr;
};
上述链表的迭代器继承了迭代器门面类,只需要提供必要的几个函数就完成了输入迭代器所需的实现。如果要实现其他迭代器的功能,只需要再提供对应所需的函数即可,而不是重载所有迭代器的接口。
美中不足的是,上述实现的链表迭代器将会把dereference
、increment
和equal
等内部函数暴露出去,可以通过再添加一个代理类来优化掉这个缺陷:
class IteratorFacadeAccess {
// 所有迭代器
template<typename Reference, typename Iterator>
static Reference dereference(const Iterator& iter) {
return iter.dereference();
}
template<typename Iterator>
static void increment(Iterator& i) {
return i.increment();
}
// 双向迭代器
template<typename Iterator>
static void decrement(Iterator& i) {
return i.decrement();
}
// 随机迭代器
template<typename Iterator, typename Distance>
static void advance(Iterator& i, Distance n) {
return i.advance(n);
}
// ...
template<typename Derived, typename Value, typename Category>
friend class IteratorFacade;
};
template<typename Derived, typename Value, typename Category>
class IteratorFacade {
// ...
// 输入迭代器接口
reference operator*() const {
return IteratorFacadeAccess::dereference<reference, Derived>(asDerived());
}
Derived& operator++() {
IteratorFacadeAccess::increment<Derived>(asDerived());
return asDerived();
}
Derived operator++(int) {
Derived result(asDerived());
IteratorFacadeAccess::increment<Derived>(asDerived());
return result;
}
friend bool operator==(const IteratorFacade& lhs, const IteratorFacade& rhs) {
return lhs.asDerived().equal(rhs.asDerived());
}
};
template<typename T>
class ListNodeIterator : public IteratorFacade<ListNodeIterator<T>, T, std::forward_iterator_tag> {
// ...
friend class IteratorFacadeAccess;
};
这样就可以把链表迭代器所需的成员函数设置为私有的,使用时通过IteratorFacadeAccess来间接访问。
int main() {
ListNode<int>* root = new ListNode<int>;
root->value = 1;
root->next = new ListNode<int>;
root->next->value = 2;
root->next->next = new ListNode<int>;
root->next->next->value = 3;
ListNodeIterator<int> iter(root);
cout << *iter << endl; // 1
iter++;
cout << *iter << endl; // 2
++iter;
cout << *iter << endl; // 3
return 0;
}
混入#
为了体现混入(mixin)的应用,我们先设想一种场景:有若干个坐标点,并定义一个处理坐标点集合的类。
struct Point {
double x, y;
Point() : x(0), y(0) { }
Point(double x, double y) : x(x), y(y) { }
};
class Polygon {
public:
// 运算方法...
private:
std::vector<Point> m_points{};
};
现在要给这些坐标点添加一些属性,自然想到的是从Point派生出一个新的类LabeledPoint,表示为每个坐标点添加一个标签属性:
struct LabeledPoint : public Point {
std::string label;
LabeledPoint() : Point(), label("") { }
LabeledPoint(double x, double y) : Point(x, y), label("") { }
};
这样的实现存在一些缺点:
- 需要将Point类公开出来,以便可以从中派生。
- 需要根据Point的构造,依次实现对应的构造函数,并且保证接口的一致性,否则将无法和Polygon类一起工作。
- 如果Point添加了新的构造函数,那么所有的派生都要进行修改。
这时混入就可以登场了,混入提供了继承之外的另一种定制类型的行为。
混入本质上是反转了一般的继承方向,即这些新类是作为类模板的的基类“混入”继承层次结构的,而不是作为一个新的派生类被创建的。
这种方式允许引入新的数据成员和运算方法,而无需重复实现任何接口。
template<typename... Mixins>
struct MixPoint : public Mixins... {
double x, y;
Point() : Mixins()..., x(0), y(0) { }
Point(double x, double y) : Mixins()..., x(x), y(y) { }
};
使用混入的方法重新定义点,就可以随意地添加属性了。
template<typename... Mixins>
struct MixPoint : public Mixins... {
double x, y;
Point() : Mixins()..., x(0), y(0) { }
Point(double x, double y) : Mixins()..., x(x), y(y) { }
};
class Label {
public:
std::string label;
Label() : label("") { }
};
class Color {
public:
unsigned char red = 0, green = 0, blue = 0;
};
using MyPoint = MixPoint<Label, Color>;
使用到对应属性时,只需要将其隐式转换为所需的混入类即可。
template<typename... Mixins>
class MixPolygon {
public:
// 公开的运算方法...
private:
std::vector<Point<Mixins...>> m_points;
};
结合CRTP的混入#
如果在混入的类再加入CRTP的技巧,就可以实现更加强大的功能,比如让混入的类可以根据情况做出不同的行为。
template<template<typename>... Mixins>
struct MixPoint : public Mixins<MixPoint>... {
double x, y;
Point() : Mixins<MixPoint>()..., x(0), y(0) { }
Point(double x, double y) : Mixins<MixPoint>()..., x(x), y(y) { }
};
参数化的虚拟性#
混入还带来了一个惊人的技巧——允许将派生类的属性进行参数化,比如成员函数的虚拟性。
class NoVirtual { };
class Virtual {
public:
virtual void foo() { }
};
template<typename... Mixins>
class Base : public Mixins... {
public:
void foo() {
std::cout << "Base::foo()\n";
}
};
template<typename... Mixins>
class Derived : public Base<Mixins...> {
public:
void foo() {
cout << "Derived::foo()\n";
}
};
int main() {
Base<NoVirtual>* p1 = new Derived<NoVirtual>;
p1->foo(); // Base::foo()
Base<Virtual>* p2 = new Derived<Virtual>;
p2->foo(); // Derived::foo()
return 0;
}
实际上,写明override等虚函数的关键字会对虚函数做出更多的检查,但是不写关键字也并不会影响虚函数的重写。当混入含有虚函数的类时,foo()
就被解释成了虚函数,而混入不含虚函数的类时,foo()
就被解释成了普通的成员函数。因此,p1调用的是基类自身的普通成员函数,而p2因为foo已经被重写了,所以调用的是派生类中的虚函数。这种可以将成员属性作为参数,在编译时重新解释的技巧确实令人惊讶。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗