基于策略的设计是著名策略模式的一种编译时变体;我们为此专门安排了一整章内容,即第15章。在此,我们将重点关注如何使用CRTP为派生类提供其他功能。具体来说,我们将推广CRTP基类的用法,以扩展派生类的接口。
目前,我们一直使用一个基类来为派生类添加功能:
template <typename D> struct plus_base {...};
class D : public plus_base<D> {...};
然而,如果我们希望以多种方式扩展派生类的接口,单一基类就会带来不必要的限制。首先,如果在基类中添加多个成员函数,该基类可能会变得非常庞大。其次,可能希望采用更模块化的方式来设计接口。
例如,可以设计一个基类模板,为派生类添加工厂构造方法:
// Example 10
template <typename D> struct Factory {
template <typename... Args>
static D* create(Args&&... args) {
return new D(std::forward<Args>(args)...);
}
static void destroy(D* d) { delete d; }
};
甚至可以有多个不同的工厂,提供相同的接口,但以不同的方式分配内存。还可以有另一个基类模板,为具有流插入操作符的类添加转换为字符串的功能:
// Example 10
template <typename D> struct Stringify {
operator std::string() const {
std::stringstream S;
S << *static_cast<const D*>(this);
return S.str();
}
};
将两者合并为一个基类毫无意义。在大型系统中,这类类可能会更多,每个类都通过 CRTP 为派生类添加特定功能。但并非每个派生类都需要所有这些功能。拥有多个基类可供选择,可以轻松地构建一个具有特定功能组合的派生类:
// Example 10
class C1 : public Stringify<C1>, public Factory<C1> {...};
这样做可行,但如果需要实现多个行为非常相似的派生类(仅在 CRTP 基类提供的功能上有所不同),就有重复编写相同代码的风险。如果有另一个工厂,在线程局部存储中构造对象,以加速并发程序的性能(我们称之为 TLFactory),可能需要编写如下代码:
class C2 : public Stringify<C2>, public TLFactory<C2> {...};
但除了基类不同外,C1 和 C2 这两个类完全相同,然而按照目前的写法,必须实现并维护两份完全相同的代码。如果能编写一个类模板,并根据需要插入不同的基类,那将会更好。这正是基于策略的设计的核心思想;实现这种设计有不同的方法,可以在第 15 章中了解更多。目前,让我们专注于如何在模板中使用 CRTP 基类。既然现在需要一个能接受多个基类类型的类模板,就必须使用可变参数模板。现在,需要类似这样的东西:
template <typename... Policies>
class C : public Policies... {};
有一些版本的基于策略的设计会使用这种确切的模板;但在我们的情况下,如果尝试将 Factory 或 Stringify 作为策略使用,代码将无法编译。原因是它们不是类型(类),不能用作类型名称。它们是模板,所以必须将模板 C 的模板参数本身声明为模板(这称为双重模板参数)。如果先回顾一下,如何声明一个单一的双重模板参数,其语法会更容易理解:
template <template <typename> class B> class C;
如果想从这个类模板 B 的某个特定实例继承,可以这样写:
template <template <typename> class B>
class C : public B<template argument> {...};
在使用 CRTP 时,模板参数是派生类自身的类型,即 C<B>:
template <template <typename> class B>
class C : public B<C<B>> {...};
直接将其推广到参数包:
// Example 11
template <template <typename> class... Policies>
class C : public Policies<C<Policies...>>... {...};
模板参数是一个参数包(任意数量的模板,而非单个类)。派生类继承自整个 Policies... 包,但 Policies 是模板,需要指明这些模板的具体实例化。参数包中的每个模板都以派生类自身进行实例化,而派生类的类型为 C<Policies...>。
如果需要其他的模板参数,例如:为了在类 C 中支持使用不同的值类型,可以将这些参数与策略结合使用:
// Example 11
template <typename T,
template <typename> class... Policies>
class C : public Policies<C<T, Policies...>>... {
T t_;
public:
explicit C(T t) : t_(t) {}
const T& get() const { return t_; }
friend std::ostream&
operator<<(std::ostream& out, const C c) {
out << c.t_;
return out;
}
};
要使用一组特定的策略来实例化这个类,定义一些类型别名会非常方便:
using X = C<int, Factory, Stringify>;
如果希望在多个类中使用相同的 Policies,也可以定义一个模板别名:
template <typename T> using Y = C<T, Factory, Stringify>;
我们将在第15章中进一步学习有关策略的更多知识。CRTP(奇异递归模板模式),将在本书的这一章以及其他章节中多次遇到 —— 它是一种灵活且强大的工具。