16.4. 适配器与策略模式对比

适配器模式和策略模式是一些较为通用的模式,而 C++ 为这些模式增添了泛型编程的能力。这会扩展其可用性,有时甚至模糊了模式之间的界限。这些模式本身定义得非常清晰 —— 策略提供自定义的实现,而适配器则改变接口并为现有接口添加功能(后者是装饰器的特性,但大多数装饰器都通过适配器实现)。

上一章也了解到,C++ 拓展了基于策略的设计能力; C++ 中策略不仅可以控制实现,还可以添加或移除接口的一部分。尽管这些模式各不相同,但在可以解决的问题类型上存在显著的重叠。当一个问题大致上适用于两种模式时,比较这两种方法是很有启发性的。在本次探讨中,我们将考虑设计一个自定义值类型的问题。

简单来说,值类型是一种行为类似于 int 的类型。这类类型通常是数值。虽然有一组内置类型可以处理数值,但可能需要操作有理数、复数、张量、矩阵,或者带有单位的数值(如米、克等)。这些值类型支持一组操作,例如算术运算、比较、赋值和复制。根据值所代表的含义,可能只需要其中一部分操作 —— 对于矩阵,可能需要支持加法和乘法,但不需要除法;在大多数情况下,比较矩阵是否相等以外的比较可能没有意义。同样,我们可能不希望允许将米与克相加。更一般地说,人们常常希望拥有一个接口受限的数值类型 —— 对于此类数值所代表的量,不允许的操作在编译时就无法通过,包含无效操作的程序根本就无法编写。为了实现通用性,我们的设计必须允许逐步构建接口。例如,可能需要一个可比较相等性、可排序(定义了小于操作)且可相加的值类型,但不需要乘法或除法。这看起来像是一个为装饰器(或更广义的适配器)模式量身定做的问题:装饰器可以添加诸如比较操作符或加法操作符之类的行为。另一方面,通过插入合适的策略来配置一组功能以创建类型,这正是策略模式的用武之地。

16.4.1 适配器解决方案

首先考察适配器方案,从一个基本的值类型开始,其接口几乎不支持功能,然后可以逐个添加所需的功能。

这是初始的 Value 类模板:

// Example 24
template <typename T> class Value {
public:
  using basic_type = T;
  using value_type = Value;

  explicit Value() : val_(T()) {}
  explicit Value(T v) : val_(v) {}

  Value(const Value&) = default;

  Value& operator=(const Value&) = default;
  Value& operator=(basic_type rhs) {
    val_ = rhs;
    return *this;
  }
protected:
  T val_ {};
};

该 Value 类型支持复制和赋值,既可以从底层类型(如 int)赋值,也可以从另一个 Value 对象赋值。如果需要创建不可复制的值类型,也可以将其中一些功能移到适配器中实现,但在介绍完整个章节后,会发现这种修改非常容易。

为了方便起见,也会让 Value 支持打印输出(实际应用中,可能希望将其设计为一个独立且可配置的功能),但这能让示例更简洁,且不会影响核心要点。

// Example 24
template <typename T> class Value {
public:
  friend std::ostream& operator<<(std::ostream& out, Value x) {
    out << x.val_;
    return out;
  }
  friend std::istream& operator>>(std::istream& in, Value& x) {
    in >> x.val_;
    return in;
  }
  ...
};

使用在第12章中描述的友元工厂模式来生成这些函数,对 Value 所能做的只是初始化,或将其赋值给另一个值,以及打印:

// Example 24
using V = Value<int>;
V i, j(5), k(3);
i = j;
std::cout << i; // 输出 5

除此之外,无法对此类进行其他操作 —— 不能进行相等或不等的比较,也没有算术运算。然而,可以创建一个适配器来添加比较接口:

// Example 24
template <typename V> class Comparable : public V {
public:
  using V::V;
  using V::operator=;
  using value_type = typename V::value_type;
  using basic_type = typename value_type::basic_type;

  Comparable(value_type v) : V(v) {}
  friend bool operator==(Comparable lhs, Comparable rhs) {
    return lhs.val_ == rhs.val_;
  }
  friend bool operator==(Comparable lhs, basic_type rhs) {
    return lhs.val_ == rhs;
  }
  friend bool operator==(basic_type lhs, Comparable rhs) {
    return lhs == rhs.val_;
  }
  ... operator!= 同上...
};

这是一个类适配器 —— 从增强的类派生,从而继承了其所有接口,并添加了更多功能 —— 即完整的比较操作符集合。处理值类型时,通常使用传值,而非引用(尽管使用 const 引用传递也并非错误,某些编译器可能会将任一版本优化为相同的结果)。

我们已经熟悉了这种适配器的使用方式:

using V = Comparable<Value<int>>;
V i(3), j(5);
i == j; // False
i == 3; // True
5 == j; // 还是 true

这添加了一项功能。还能添加更多功能吗?没问题 —— Ordered 适配器的编写方式非常相似,只是它提供的是 <、<=、> 和 >= 操作符(或者在 C++20 中,提供 operator<=>):

// Example 24
template <typename V> class Ordered : public V {
public:
  using V::V;
  using V::operator=;
  using value_type = typename V::value_type;
  using basic_type = typename value_type::basic_type;

  Ordered(value_type v) : V(v) {}
  friend bool operator<(Ordered lhs, Ordered rhs) {
    return lhs.val_ < rhs.val_;
  }
  friend bool operator<(basic_type lhs, Ordered rhs) {
    return lhs < rhs.val_;
  }
  friend bool operator<(Ordered lhs, basic_type rhs) {
    return lhs.val_ < rhs;
  }
  ... 其他操作符同理 ...
};

可以组合这两个适配器,并且可以以任意顺序组合使用:

using V = Ordered<Comparable<Value<int>>>;
// 或 Comparable<Ordered<...>
V i(3), j(5);
i == j; // False
i <= 3; // True

有些操作或功能需要更多的工作。如果值类型是数值型的,比如 Value<int>,就会希望支持一些算术运算,如加法和乘法。以下是一个启用加法和减法的装饰器:

// Example 24
template <typename V> class Addable : public V {
public:
  using V::V;
  using V::operator=;
  using value_type = typename V::value_type;
  using basic_type = typename value_type::basic_type;

  Addable(value_type v) : V(v) {}
  friend Addable operator+(Addable lhs, Addable rhs) {
    return Addable(lhs.val_ + rhs.val_);
  }
  friend Addable operator+(Addable lhs, basic_type rhs) {
    return Addable(lhs.val_ + rhs);
  }
  friend Addable operator+(basic_type lhs, Addable rhs) {
    return Addable(lhs + rhs.val_);
  }
  ... 其他操作符同理 ...
};

这个装饰器使用起来非常简单:

using V = Addable<Value<int>>;
V i(5), j(3), k(7);
k = i + j; // 8

也可以将 Addable 与其他装饰器组合使用:

using V = Addable<Ordered<Value<int>>>;
V i(5), j(3), k(7);
if (k - 1 < i + j) { ... }

但我们遇到了一个问题,这个问题只是好运没碰到:

using V = Ordered<Addable<Value<int>>>;
V i(5), j(3), k(7);
if (k - 1 < i + j) { ... }

这个例子与前一个例子本应没有区别,但在最后一行会出现一个编译错误:没有有效的 operator< 可用。问题在于 i + j 表达式使用了来自 Addable 适配器的 operator+(),而该操作符返回的是 Addable<Value<int>> 类型的对象。但比较操作符期望的是 Ordered<Addable<Value<int>>> 类型,不会接受这个“不完整”的类型(不存在从基类到派生类的隐式转换)。

一个不令人满意的解决方案是,要求 Addable 必须始终是最外层的装饰器。这不仅感觉上很别扭,而且作用有限:接下来想要的装饰器可能是 Multipliable,也会遇到同样的问题。当某个类型同时是 Addable 和 Multipliable 时,不可能让两者都处于最外层。

需要注意的是,之前使用返回 bool 的比较操作符时没有遇到问题,但当需要返回装饰后的类型本身(如 operator+() 所做的那样),可组合性就崩溃了。

要解决这个问题,每个返回装饰类型的操作符,都必须返回原始的(最外层的)类型。如果值类型是 Ordered<Addable<Value<int>>>,两个值相加的结果也应该具有相同的类型。当然,问题在于 operator+() 由 Addable 装饰器提供,而 Addable 只知道自己及其基类,需要一个中间类(Addable<...>)来返回其派生类(Ordered<Addable<...>>)类型的对象。

这是一个非常常见的设计问题,有一个专门的模式来解决它:奇异递归模板模式。将此模式应用于装饰器需要一些递归思维。我们将介绍两个主要思想,然后需要处理一个相当大的代码示例。

首先,每个装饰器将有两个模板参数。第一个参数和之前一样:它是链中的下一个装饰器,或者在链的末端是 Value<int>(当然,该模式不仅限于 int,但为了简化示例,始终使用相同的基类型)。第二个参数将是最终的外层类型;我们称之为“最终值类型”,所有的装饰器都将声明如下:

template <typename V, typename FV> class Ordered : ...

但在代码中,仍然希望写成:

using V = Ordered<Addable<Value<int>>>;

所以需要为第二个模板参数提供一个默认值。这个值可以在装饰器中,不会在其他地方使用的类型;void 就是一个不错的选择。还需要为这个默认类型提供一个偏特化版本,如果最终值类型没有显式指定,就必须以某种方式推导出来:

template <typename V, typename FV = void> class Ordered;
template <typename V> class Ordered<V, void>;

现在,让逐步分析“嵌套”类型 Ordered<Addable<Value<int>>>。最外层,可以将其视为 Ordered<T>,其中 T 是 Addable<Value<int>>。由于没有为 Ordered 模板指定第二个类型参数 FV,将得到默认值 void,模板实例化 Ordered<T> 将使用 Ordered 模板的偏特化版本。尽管没有明确指定“最终值类型” FV,但我们知道它是什么:就是 Ordered<T> 本身。

接下来,需要确定要继承的基类。由于每个装饰器都继承自它所装饰的类型,应该是 T,即 Addable<U>(其中 U 是 Value<int>)。但这行不通:需要将正确的最终值类型传递给 Addable。因此,应该继承自 Addable<U, FV>,其中 FV 是最终值类型 Ordered<T>。不幸的是,代码中并没有写 Addable<U, FV>,只有 Addable<U>。我们需要的是某种方法,来找出由同一个模板 Addable 但使用不同的第二个模板参数(Ordered<T> 而不是默认的 void)生成的类型。

这是 C++ 模板中一个非常常见的问题,它有一个同样常见的解决方案:模板重新绑定。每个装饰器模板都需要定义以下模板别名:

template <typename V, typename FV = void>
class Ordered : public ... 一些基类 ... {
public:
  template <typename FV1> using rebind = Ordered<V, FV1>;
}

现在,给定类型 T(它是某个装饰器模板的实例化),可以找出由同一个模板但使用不同的第二个模板参数 FV 生成的类型:即 T::template rebind<FV>。这正是 Ordered<V> 需要继承的类型,以便将正确的最终值类型传递给下一个装饰器:

// Example 25
template <typename V, typename FV = void>
class Ordered : public V::template rebind<FV> { ... };

这个类模板的含义是:给定一个类型 Ordered<T, FV>,将从类型 T 重新绑定到相同的最终值类型 FV 的结果类型继承,并忽略 T 的第二个模板参数。唯一的例外是最外层的类型,此时模板参数 FV 为 void,但我们知道最终值类型应该是什么,因此可以重新绑定到该类型:

// Example 25
template <typename V> class Ordered<V, void> :
  public V::template rebind<Ordered<V>> { ... };

注意这里的语法,需要使用 template 关键字:一些编译器可能会接受 V::rebind<Ordered<V>>,但这是错误的,标准要求使用确切的语法 V::template rebind<Ordered<V>>。

现在可以将所有内容整合起来。在一般情况下,当装饰器位于装饰器链的中间时,必须将最终值类型传递给基类:

// Example 25
template <typename V, typename FV = void>
class Ordered : public V::template rebind<FV> {
  using base_t = typename V::template rebind<FV>;
public:
  using base_t::base_t;
  using base_t::operator=;
  template <typename FV1> using rebind = Ordered<V, FV1>;
  using value_type = typename base_t::value_type;
  using basic_type = typename value_type::basic_type;
  explicit Ordered(value_type v) : base_t(v) {}
  friend bool operator<(FV lhs, FV rhs) {
    return lhs.val_ < rhs.val_;
  }
  ... 其余的操作符 ...
};

类型别名 base_t 是为了方便而引入的,编写 using 语句更加容易。需要在依赖于模板参数的类型前加上 typename 关键字;不需要在指定基类时写 typename,基类始终是一个类型,所以 typename 是多余的。

最外层类型的特殊情况(最终值类型未指定,默认为 void)与此非常相似:

// Example 25
template <typename V> class Ordered<V, void>
  : public V::template rebind<Ordered<V>> {
  using base_t = typename V::template rebind<Ordered>
public:
  using base_t::base_t;
  using base_t::operator=;
  template <typename FV1> using rebind = Ordered<V, FV1>;
  using value_type = typename base_t::value_type;
  using basic_type = typename value_type::basic_type;
  explicit Ordered(value_type v) : base_t(v) {}
  friend bool operator<(Ordered lhs, Ordered rhs) {
    return lhs.val_ < rhs.val_;
  }
  ... 其余的操作符 ...
};

这个特化版本与一般情况在两个方面有所不同。除了基类之外,操作符的参数类型也不能是 FV,此时 FV 是 void。相反,必须使用由模板生成的类的类型,在模板定义内部,可以直接将其称为 Ordered(在类内部使用模板名称时,指的是具体的实例化 —— 不需要重复模板参数)。

对于那些操作符返回值的装饰器,需要确保始终为返回类型使用正确的最终值类型。在一般情况下,这就是第二个模板参数 FV:

// Example 25
template <typename V, typename FV = void>
class Addable : public V::template rebind<FV> {
  friend FV operator+(FV lhs, FV rhs) {
    return FV(lhs.val_ + rhs.val_);
  }
  ...
};

而在最外层装饰器的特化版本中,最终值类型就是装饰器本身:

// Example 25
template <typename V>
class Addable<V, void> : public V::template rebind<FV> {
  friend Addable operator+(Addable lhs,Addable rhs) {
    return Addable(lhs.val_ + rhs.val_);
  }
  ...
};

我们必须将这种技术应用于每一个装饰器模板。现在,可以以任意顺序组合这些装饰器,并定义具有可用操作子集的值类型:

// Example 25
using V = Comparable<Ordered<Addable<Value<int>>>>;

// Addable<Ordered<Comparable<Value<int>>>> 这样也行
V i, j(5), k(3);

i = j; j = 1;

i == j; // 可以 – 支持比较
i > j; // 可以 – 支持排序
i + j == 7 – k; // 可以 – 支持比较和加法
i*j; // 不支持乘法 – 无法编译

所有的装饰器,都为类添加了成员或非成员操作符。我们也可以添加成员函数,甚至构造函数。后者在想要添加转换时非常有用。例如,可以添加一个从底层类型隐式转换的功能(Value<T> 不能从 T 隐式构造)。转换装饰器遵循与其他装饰器相同的模式,但添加了一个隐式转换构造函数:

// Example 25
template <typename V, typename FV = void>
class ImplicitFrom : public V::template rebind<FV> {
  ...
  explicit ImplicitFrom(value_type v) : base_t(v) {}
  ImplicitFrom(basic_type rhs) : base_t(rhs) {}
};

template <typename V> class ImplicitFrom<V, void> :
public V::template rebind<ImplicitFrom<V>> {
  ...
  explicit ImplicitFrom(value_type v) : base_t(v) {}
  ImplicitFrom(basic_type rhs) : base_t(rhs) {}
};

现在,可以对值类型使用隐式转换,例如在调用函数时:

using V = ImplicitFrom<Ordered<Addable<Value<int>>>>;
void f(V v);
f(3);

如果想实现向底层类型的隐式转换,可以使用一个非常相似的适配器,但它添加的是转换操作符,而非构造函数:

// Example 25
template <typename V, typename FV = void>
class ImplicitTo : public V::template rebind<FV> {
  ...
  explicit ImplicitTo(value_type v) : base_t(v) {}
  operator basic_type(){ return this->val_; }
  operator const basic_type() const { return this->val_; }
};

template <typename V>
class ImplicitTo<V, void> : public V::template rebind<ImplicitTo<V>> {
  ...
  explicit ImplicitTo(value_type v) : base_t(v) {}
  operator basic_type(){ return this->val_; }
  operator const basic_type() const { return this->val_; }
};

可以进行反向的转换:

using V = ImplicitTo<Ordered<Addable<Value<int>>>>;
void f(int i);
V i(3);
f(i);

这种设计能够完成任务,除了编写适配器本身的复杂性之外,并没有特别的问题:CRTP 的递归应用往往会让人陷入思维上的无限循环,直到习惯于思考这种模板适配器。另一种替代方案是基于策略的值类型设计。

16.4.2 策略解决方案

我们现在将探讨一种,与第15章中所见略有不同的策略设计形式。它没有那么通用,但当适用时,可以在避免一些问题的同时,提供策略模式的所有优势,特别是可组合性。

问题仍然相同:创建一个具有可控制操作集的自定义值类型。这个问题可以用标准基于策略的方法来解决:

template <typename T, typename AdditionPolicy,
          typename ComparisonPolicy,
          typename OrderPolicy,
          typename AssignmentPolicy, ... >
class Value { ... };

这种实现方式会遇到了基于策略设计的所有缺点 —— 策略列表很长,所有策略都必须明确写出,并且没有好的默认值;策略按位置排列,类型声明需要仔细数逗号,而且随着新策略的添加,策略之间有意义的顺序都会消失。我们并未提及不同策略集会产生不同类型的问题 —— 在这种情况下,这并非缺点,而是设计意图。如果想要一个支持加法的类型和一个类似,但不支持加法的类型,那么必须是不同的类型。

理想情况下,只希望列出希望值具备的属性所对应的策略 —— 想要一个基于整数的值类型,它支持加法、乘法和赋值,但不支持其他操作。毕竟,之前通过适配器模式实现了,因此现在也不会接受更低的标准。

首先,让思考一下这样的策略应该是什么样子。启用加法的策略应该将 operator+() 注入到类的公共接口中(可能还包括 operator+=())。使值可赋值的策略应该注入 operator=()。它们必须是基类,通过公有继承,并且需要知道派生类是什么,并将其转换为相应的类型,因此必须使用 奇异递归模板模式:

template <
  typename T, // 基础类型(例如 int)
  typename V> // 派生类
struct Incrementable {
  V operator++() {
    V& v = static_cast<V&>(*this);
    ++v.val_; // 派生类内部的值
    return v;
  }
};

现在,需要仔细考虑如何在主模板中使用这些策略。首先,希望支持任意数量、任意顺序的策略。这让我们想到了可变参数模板,但为了使用 CRTP,模板参数本身必须是模板。接着,无论有多少个策略,都希望从每个模板的实例化中继承。因此,需要一个带有双重模板参数包的可变参数模板:

// Example 26
template <typename T,
          template <typename, typename> class ... Policies>
class Value :
  public Policies<T, Value<T, Policies ... >> ...
{ ... };

上述声明引入了一个名为 Value 的类模板,至少包含一个类型参数,以及零个或多个模板策略,这些策略本身具有两个类型参数(在 C++17 中,也可以用 typename ... Policies 代替 class ... Policies)。Value 类将这些模板用类型 T 和其自身进行实例化,并公开继承它们。

Value 类模板应包含我们希望所有值类型共有的接口。其余部分将由策略提供。默认使这些值可复制、可赋值和可打印:

// Example 26
template <typename T,
          template <typename, typename> class ... Policies>
class Value :
public Policies<T, Value<T, Policies ... >> ...
{
public:
  using base_type = T;
  explicit Value() = default;
  explicit Value(T v) : val_(v) {}
  Value(const Value& rhs) : val_(rhs.val_) {}
  Value& operator=(Value rhs) {
    val_ = rhs.val_;
    return *this;
  }
  Value& operator=(T rhs) { val_ = rhs; return *this; }
  friend std::ostream&
  operator<<(std::ostream& out, Value x) {
    out << x.val_; return out;
  }
  friend std::istream&
    operator>>(std::istream& in, Value& x) {
    in >> x.val_; return in;
  }
private:
  T val_ {};
};

再次,使用第12章中的友元工厂来生成流操作符。

在着手实现所有策略之前,还面临一个障碍。Value 类中的 val_ 值是私有的,希望保持这种状态,但策略需要访问和修改它。过去,可通过将每个需要此类访问的策略声明为友元,来解决这个问题。但这一次,我们甚至不知道可能拥有的策略的名称。在读者了解了前述将参数包展开为一组基类的声明后,可能会合理地期望能变出一只兔子,以某种方式将整个参数包声明为友元。遗憾的是,标准并未提供这样的方法。我们能提出的最佳解决方案是提供一组访问器函数,这些函数应仅由策略调用,但没有好的方法来强制执行(一个类似 policy_accessor_do_not_call() 的名称可能在一定程度上暗示用户代码应远离它,但开发者的创造力是无穷的,这类提示并非总被遵守):

// Example 26
template <typename T,
            template <typename, typename> class ... Policies>
class Value :
public Policies<T, Value<T, Policies ... >> ...
{
public:
  ...
  T get() const { return val_; }
  T& get() { return val_; }
private:
  T val_ {};
};

要创建一个具有受限操作集的值类型,必须使用所需的一组策略(且仅此而已)来实例化此模板:

// Example 26
using V = Value<int, Addable, Incrementable>;
V v1(0), v2(1);
v1++; // 支持自增 - 可以编译
V v3(v1 + v2); // 支持加法 - 可以编译
v3 *= 2; // 未包含乘法策略 - 无法编译

可以实现的策略的数量和类型主要受限于实际需求(或想象力),但以下是一些示例,展示了如何向类中添加不同类型的操作。

首先,可以实现前述的 Incrementable 策略,它提供两个 ++ 操作符:后置和前置:

// Example 26
template <typename T, typename V> struct Incrementable {
  V operator++() {
    V& v = static_cast<V&>(*this);
    ++(v.get());
    return v;
  }

  V operator++(int) {
    V& v = static_cast<V&>(*this);
    return V(v.get()++);
  }
};

可以为 -- 操作符单独创建一个 Decrementable 策略,如果对类型有意义,也可以将两者合并为一个策略。此外,如果想以非1的值进行递增,那么还需要 += 操作符:

// Example 26
template <typename T, typename V> struct Incrementable {
  V& operator+=(V val) {
    V& v = static_cast<V&>(*this);
    v.get() += val.get();
    return v;
  }

  V& operator+=(T val) {
    V& v = static_cast<V&>(*this);
    v.get() += val;
    return v;
  }
};

上述策略提供了两种版本的 operator+=() —— 一种接受相同 Value 类型的增量,另一种接受基础类型 T 的增量。这并非强制要求,可以根据需要实现对其他类型值的递增。甚至可以有多个版本的递增策略,只要只使用其中一个即可(如果引入了相同操作符的不兼容重载,编译器会进行提醒)。

可以以类似的方式添加 *= 和 /= 操作符。添加二元操作符(如比较操作符、加法和乘法)则略有不同 —— 这些操作符必须是非成员函数,以便允许对第一个参数进行类型转换。同样,友元工厂模式在这里非常有用。先从比较操作符开始:

// Example 26
template <typename T, typename V> struct ComparableSelf {
  friend bool operator==(V lhs, V rhs) {
    return lhs.get() == rhs.get();
  }

  friend bool operator!=(V lhs, V rhs) {
    return lhs.get() != rhs.get();
  }
};

实例化此模板后,会生成两个非成员非模板函数,即针对特定 Value 类(即实例化的类)类型的变量的比较操作符。

可能还希望允许与基础类型(如 int)进行比较:

template <typename T, typename V> struct ComparableValue {
  friend bool operator==(V lhs, T rhs) {
    return lhs.get() == rhs;
  }

  friend bool operator==(T lhs, V rhs) {
    return lhs == rhs.get();
  }

  friend bool operator!=(V lhs, T rhs) {
    return lhs.get() != rhs;
  }

  friend bool operator!=(T lhs, V rhs) {
    return lhs != rhs.get();
  }
};

大多数情况下,可能希望同时拥有这两种比较类型。可以简单地将它们都放入同一个策略中,而不必担心将它们分开;或者,可以将已有的两个策略组合成一个复合策略:

// Example 26
template <typename T, typename V>
struct Comparable : public ComparableSelf<T, V>,
                    public ComparableValue<T, V> {};

在前一节中,从一开始就在单个适配器中组合了所有比较操作。在这里,采用了一种略有不同的方法,只是为了说明使用策略或适配器控制类接口的不同选项(两种解决方案提供的选项是相同的)。加法和乘法操作符也通过类似的策略创建,它们也是友元非模板非成员函数。唯一的区别是返回值类型 —— 返回对象本身,例如:

// Example 26
template <typename T, typename V> struct Addable {
  friend V operator+(V lhs, V rhs) {
    return V(lhs.get() + rhs.get());
  }

  friend V operator+(V lhs, T rhs) {
    return V(lhs.get() + rhs);
  }

  friend V operator+(T lhs, V rhs) {
    return V(lhs + rhs.get());
  }
};

在编写适配器时,遇到的返回“最终值类型”的难题在这里不复存在:传递给每个策略的派生类本身就是值类型。

同样可以轻松添加,用于转换为基础类型的显式或隐式转换操作符:

// Example 26
template <typename T, typename V>
struct ExplicitConvertible {
  explicit operator T() {
    return static_cast<V*>(this)->get();
  }

  explicit operator const T() const {
    return static_cast<const V*>(this)->get();
  }
};

这种做法乍一看,似乎解决了传统基于策略类型设计的大部分缺点。策略的顺序不再重要 —— 只需指定所需策略,无需关心其他策略 —— 这有什么不好呢?然而,它存在两个根本性的限制。

首先,基于策略的类无法通过名称引用策略。不再有 DeletionPolicy 或 AdditionPolicy 这样的插槽。也没有约定强制的策略接口,例如删除策略必须是可调用的。将策略绑定到单一类型的整个过程是隐式的;它仅仅是接口的叠加。

因此,使用这些策略所能做的事情是有限的 —— 可以注入公有成员函数和非成员函数,甚至添加私有数据成员 —— 但无法为由主策略基类确定和限制的行为方面提供实现。因此,这并不是策略模式的实现 —— 可随意组合接口,进而强制组合实现,而不是定制特定算法(这也就是为什么,我们将这种替代性策略设计模式的演示推迟到本章的原因)。

第二个密切相关的问题是,没有默认策略。缺失的策略就是缺失了,没有替代。默认行为始终是没有行为。在传统的策略设计中,每个策略插槽都必须填充。如果有合理的默认值,可以指定它,那么除非用户覆盖,否则这就是策略(例如,默认删除策略使用 operator delete)。如果没有默认值,编译器不会允许省略该策略 —— 必须为模板提供一个参数。

这些限制的影响很深远。例如,可能有人会想使用我们在第15章中看到的 enable_if 技术,而不是通过基类注入公有成员函数。这样,如果其他选项都未启用,可以有一个默认行为。但在这里行不通,当然可以创建一个专为与 enable_if 一起使用而设计的策略:

template <typename T, typename V> struct Addable {
  constexpr bool adding_enabled = true;
};

但无法使用它 —— 不能使用 AdditionPolicy::adding_enabled,因为根本不存在 AdditionPolicy —— 所有策略插槽都是无名的。另一个选择是使用 Value::adding_enabled —— 加法策略是 Value 的一个基类,其所有数据成员在 Value 类中都是可见的。唯一的问题是这行不通 —— 在编译器评估此表达式时(定义 Value 类型作为 CRTP 策略的模板参数时),Value 仍是一个不完整的类型,还无法访问其数据成员。如果知道策略名称,就可以评估 policy_name::adding_enabled。但这种知识正是为了不必指定完整策略列表而放弃的。

严格来说,虽然这并非策略模式的应用,但刚刚了解的这种替代性基于策略设计,在策略主要用于控制一组支持的操作时,可能颇具吸引力。在讨论基于策略设计的指导原则时,仅为了提供受限接口的安全性而使用一个策略插槽,通常意义不大。对于这种情况,应使用这种替代方法。

总体而言,可以看到两种模式各有优缺点:适配器依赖于更复杂的 CRTP 形式,而刚刚看到的“无插槽”策略则要求牺牲封装性(必须通过类似 get() 的方法将值暴露给策略)。这正是我们作为软件工程师需要解决的问题的本质 —— 当问题变得足够复杂,通常可以用不止一种设计来解决,而每种方法都有其自身的优点和限制。

我们无法在一本有限篇幅的书中,对所有可用于创建两种截然不同设计以满足相同需求的模式进行详尽比较。通过呈现和分析这些示例,我们希望为读者提供理解和洞察力,帮助他们在面对现实问题时评估同样复杂多样的设计选项。