13.3. 工厂模式

我们所面临的问题 —— 即如何在运行时决定创建某一特定类型的对象 —— 显然是一个非常常见的设计问题。设计模式正是为解决这类问题而生的,而这个问题也有其对应的设计模式 —— 它被称为工厂模式。

工厂模式是一种创建型模式,它为多个相关问题提供了解决方案:如何将创建哪个对象的决策委派给派生类,如何使用一个独立的工厂方法来创建对象等。我们将逐一回顾工厂模式的这些变体,首先从最基本的工厂方法开始。

13.3.1 工厂方法基础

最简单的形式中,工厂方法会构造一个在运行时指定类型的对象:

class Base { ... };
class Derived : public Base { ... };
Base* p = ClassFactory(type_identifier, ... arguments );

如何在运行时确定要创建哪个对象?需要为工厂能够创建的每种类型提供一个运行时标识符。在最简单的情况下,这些类型的列表在编译时是已知的。

考虑一个游戏设计,玩家从菜单中选择要建造的建筑类型。程序拥有一个可建造建筑的列表,每个建筑由一个对象表示,并为每个建筑分配一个标识符:

// Example 05
enum Buildings {
  FARM, FORGE, MILL, GUARDHOUSE, KEEP, CASTLE
};

class Building {
public:
  virtual ~Building() {}
};

class Farm : public Building { ... };
class Forge : public Building { ... };

当玩家选择建筑类型时,游戏程序也会选择相应的标识符值。现在,程序可以使用工厂方法来构造建筑:

Building* new_farm = MakeBuilding(FARM);

该工厂接收类型标识符作为参数,并返回指向基类的指针。返回的对象类型应与该标识符对应的类型一致。

这个工厂是如何实现的呢?请记住上一节的结论 —— 在程序中的某个地方,每个对象都必须使用其真实类型进行显式构造。工厂模式并不会消除这一要求,只是将构造发生的位置隐藏起来:

// Example 05
Building* MakeBuilding(Buildings building_type) {
  switch (building_type) {
    case FARM: return new Farm;
    case FORGE: return new Forge;
    ...
  }
}

类型标识符与对象类型之间的对应关系被编码在工厂内部的 switch 语句中。由于只有一个工厂方法,且其类型在编译时就已声明,工厂所构造的所有类型的返回类型必须相同。在最简单的情况下,返回类型是基类指针。不过,如果遵循本书第3章中描述的现代内存所有权惯用法,工厂应返回指向基类的唯一指针,即 std::unique_ptr<Building>。

// Example 06:
class Building {
  public:
  enum Type {FARM, FORGE, ...};
  virtual ~Building() {}
  auto MakeBuilding(Type building_type);
};

auto Building::MakeBuilding(Type building_type) {
  using result_t = std::unique_ptr<Building>;
  switch (building_type) {
  case FARM: return result_t{new Farm};
  case FORGE: return result_t{new Forge};
  ...
  }
}

在极少数确实需要共享所有权的情况下,可以通过将对象从唯一指针转移来构造共享指针 std::shared_ptr<Building>(但这应由调用者决定,而非工厂本身)。

在此做出的另一个设计选择(与是否使用拥有型指针无关),是将类型标识符和工厂函数移入基类中。这有助于封装,并使所有相关代码和类型更加集中。

这是工厂方法的基本形式。它有许多变体,使其更适用于特定问题。我们将在下文回顾其中一些变体。

13.3.2 工厂方法说明

术语“工厂方法”的使用存在一些歧义。本章中,用它来描述一种根据某些运行时信息创建不同类型对象的函数。

还存在另一种有时也使用相同名称、但与此无关的设计模式:这种模式不是构造不同的类,而是以不同的方式构造同一个类。以下是一个简短的示例:假设有一个类用来表示平面上的一个点。该点由其坐标 x 和 y 描述:

class Point {
  double x_ {};
  double y_ {};
public:
  Point(double x, double y) : x_(x), y_(y) {}
};

目前一切正常,但同一个点也可以用例如极坐标来描述。由于这是描述同一点的两种方式,不需要一个单独的类,但可能需要一个新的构造函数,能根据指定的极坐标创建一个笛卡尔坐标点:

class Point() {
  ...
  Point(double r, double angle);
};

但这行不通:这个新构造函数和原始的从 x、y 坐标创建的构造函数接受完全相同的参数类型,重载解析无法判断你到底想调用哪一个。

一种解决方案是为不同单位的量使用不同的类型(在我们的例子中是长度和角度),但必须是真正不同的类型,而不仅仅是类型别名。有时,这样的单位模板库满足需求,但如果坚持使用 double 类型,就需要其他方式来根据调用者的意图(而不仅仅是参数)调用不同的构造方式。

解决此问题的一种方法是改用工厂构造。我们将不再直接使用构造函数,而是通过静态工厂方法来构造所有的 Point 对象。采用这种方法时,通常会将构造函数本身设为私有:

// Example 07
class Point {
  double x_ {};
  double y_ {};
  Point(double x, double y) : x_(x), y_(y) {}

public:
  static Point new_cartesian(double x, double y) {
    return Point(x, y);
  }

  static Point new_polar(double r, double phi) {
    return Point(r*std::cos(phi), r*std::sin(phi));
  }
};

Point p1(Point::new_cartesian(3, 4));
Point p2(Point::new_polar(5, 0.927295));

这种设计是可行的,但在现代 C++ 中,更流行的做法是使用多个构造函数,并通过明确定义的类型标签来消除歧义:

// Example 08
class Point {
  double x_ {};
  double y_ {};
public:
  struct cartesian_t {} static constexpr cartesian {};
  Point(cartesian_t, double x, double y) : x_(x), y_(y) {}
  struct polar_t {} static constexpr polar {};
  Point(polar_t, double r, double phi) :
    Point(cartesian, r*std::cos(phi), r*std::sin(phi)) {}
};

Point p1(Point::cartesian, 3, 4);
Point p2(Point::polar, 5, 0.927295);

创建了两个唯一类型 Point::polar_t 和 Point::cartesian_t,以及对应的变量,并将它们作为标签来指明我们想要的构造方式。由于每个构造函数的首个参数类型都唯一,重载不再有歧义。委托构造函数使这种方法更具吸引力。

虽然使用静态函数以不同方式构造同一类型的对象有时称为“工厂方法”,但也可以看作是构建器模式的一种变体(尤其是当使用一个具有类似方法的独立构建器类而非静态方法时)。无论如何,更现代的标签方法可以取代这两种模式。

在澄清了术语之后,回到最初的问题:如何根据运行时信息构造不同类型的对象。

13.3.3 工厂方法的参数

之前的简单示例中,构造函数不接受参数。但如果不同类型的构造函数需要不同的参数,向构造函数传递参数就会成为一个问题 —— MakeBuilding() 函数必须用某些特定的参数进行声明。

一个看似直接的解决方案是将工厂设为可变参数模板,并将参数直接转发给各个构造函数。一个直接的实现可能如下所示:

// Example 09
template <typename... Args>
auto Building::MakeBuilding(Type type, Args&&... args) {
  using result_t = std::unique_ptr<Building>;
  switch (type) {
    case FARM: return
      result_t{new Farm(std::forward<Args>(args)...)};
    case FORGE: return
      result_t{new Forge(std::forward<Args>(args)...)};
    ...
  }
}

这段代码可能在一段时间内都能编译通过,但迟早会遇到以下错误。为创建的两个类添加一些构造函数参数:

// Example 09
class Farm : public Building {
public:
  explicit Farm(double size);
};

class Forge : public Building {
public:
  static constexpr size_t weaponsmith = 0x1;
  static constexpr size_t welder = 0x2;
  static constexpr size_t farrier = 0x4;
  Forge(size_t staff, size_t services);
};

std::unique_ptr<Building> forge =
  Building::MakeBuilding(Building::FORGE, 2,
    Forge::weaponsmith | Forge::welder | Forge::farrier);

Forge 类使用位掩码作为标志,来标记铁匠铺提供的服务(一种处理少量非互斥选项的简单高效方案)。例如,如果 (services & Forge::farrier) 为真,则表示在铁匠铺工作的两名工匠中有一人可以为马匹钉蹄铁。这看起来简单、优雅……但无法编译。

编译器报错会指出,没有匹配的 Farm 类构造函数能够用两个整数来构造 Farm。但我们根本不是要构造一个 Farm!在编译时,无法确定是否真的要构造一个 Farm:这是一个运行时的决定。MakeBuilding() 函数必须能够编译,所以整个实现都必须通过编译,包括以 case FARM: 开头的那一行。有读者可能首先想到用 if constexpr 替代 switch 语句,但这行不通,因为用来选择构建哪个类的条件不是 constexpr,而是一个运行时的值 —— 而这正是工厂模式的核心所在。

试图用本应传给 Forge 的参数来构造 Farm 是一个错误,但这是一个运行时错误,只能在运行时被检测到。这仍然留下了一个问题:如何让那些永远不想执行的代码在语法上合法。问题在于,Farm 类没有一个构造函数可以接受所有错误的参数(但希望永远不会调用)。最简单的解决方法是提供一个这样的构造函数:

// Example 09
class Farm : public Building {
public:
  explicit Farm(...) { abort(); }
  ...
};

我们必须对工厂可能构造的所有类型,都进行同样的操作。这个可变参数函数构造函数是一种“最后 resort 的重载” —— 只有在没有其他重载匹配参数时才会选中。由于它可以匹配参数,编译错误将消失,但如果程序中确实出现问题,则会转为运行时错误。为什么不直接将这个构造函数添加到基类中呢?当然可以,但基类的构造函数在派生类中不可见,除非使用 using 语句显式引入,因此仍然需要在每个派生类中添加一些东西。

仅仅为了能与工厂创建模式配合使用,就不得不修改每一个类,这显然是一个缺点,尤其是因为这个新构造函数可能在其他地方使用,而不仅限于工厂函数中(可能会带来不幸的后果)。像往常一样,通过再增加一层间接性,也可以解决这个问题,方法是引入一个用于构造对象的重载模板:

// Example 10
template <typename T, typename... Args>
auto new_T(Args&&... args) ->
decltype(T(std::forward<Args>(args)...))* {
  return new T(std::forward<Args>(args)...);
}

template <typename T>
T* new_T(...) { abort(); return nullptr; }

template <typename... Args>
auto Building::MakeBuilding(Type type, Args&&... args) {
  using result_t = std::unique_ptr<Building>;
  switch (type) {
    case FARM: return
      result_t{new_T<Farm>(std::forward<Args>(args)...)};
    case FORGE: return
      result_t{new_T<Forge>(std::forward<Args>(args)...)};
    ...
  }
}

好消息是,现在我们不再需要修改类:使用正确参数的工厂调用都能编译通过,并转发到正确的构造函数;而使用错误参数创建对象的尝试则会导致运行时错误。坏消息是,使用错误参数创建对象的尝试都会导致运行时错误。这包括我们永远不打算执行的死代码(例如,用铁匠铺的参数创建农场),也包括我们在调用工厂时可能犯的错误。

如果在开始实现后发现可变参数模板方案远没有最初看起来那么吸引人,还有一个更简单的选择:创建一个参数对象,其层次结构与要创建的对象的层次结构相匹配。假设在游戏中,玩家可以为每个要建造的建筑选择升级。用户界面当然需要提供特定于建筑的选项,而用户选择的结果则存储在一个特定于建筑的对象中:

// Example 11
struct BuildingSpec {
  virtual Building::Type type() const = 0;
};

struct FarmSpec : public BuildingSpec {
  Building::Type type() const override {
    return Building::FARM;
  }

  bool with_pasture;
  int number_of_stalls;
};

struct ForgeSpec : public BuildingSpec {
  Building::Type type() const override {
    return Building::FORGE;
  }

  bool magic_forge;
  int number_of_apprentices;
};

我们将类型标识符包含在了参数对象中,没有理由再用两个必须始终正确匹配的参数来调用工厂方法;这只会增加出错的可能性。通过这种方式,可以确保每次工厂调用中类型标识符和参数都匹配:

// Example 11
auto Building::MakeBuilding(const BuildingSpec& spec) {
  using result_t = std::unique_ptr<Building>;
  switch (spec.type()) {
    case FARM: return result_t{
      new Farm(static_cast<const FarmSpec&>(spec))};
    case FORGE: return result_t{
      new Forge(static_cast<const ForgeSpec&>(spec))};
    ...
  }
}

工厂模式通常与我们在第9章中看到的命名参数模式配合得很好,可以避免指定冗长的参数列表。这些 spec 对象本身就可以成为用来指定命名参数的选项对象:

// Example 11
class FarmSpec {
  ...
  bool with_pasture {};
  int number_of_stalls {};
  FarmSpec() = default;
  FarmSpec& SetPasture(bool with_pasture) {
    this->with_pasture = with_pasture;
    return *this;
  }
  FarmSpec& SetStalls(int number_of_stalls) {
    this->number_of_stalls = number_of_stalls;
    return *this;
  }
};

struct ForgeSpec : public BuildingSpec {
  ...
  bool magic_forge {};
  int number_of_apprentices {};
  ForgeSpec() = default;
  ForgeSpec& SetMagic(bool magic_forge) {
    this->magic_forge = magic_forge;
    return *this;
  }
  ForgeSpec& SetApprentices(int number_of_apprentices) {
    this->number_of_apprentices = number_of_apprentices;
    return *this;
  }
};

...
std::unique_ptr<Building> farm =
  Building::MakeBuilding(FarmSpec()
                        .SetPasture(true)
                        .SetStalls(2));

std::unique_ptr<Building> forge =
  Building::MakeBuilding(ForgeSpec()
                        .SetMagic(false)
                        .SetApprentices(4));

这种技术可以与接下来几节中介绍的其他一些工厂变体相结合,以便在构造函数需要参数时能够传递参数。

13.3.4 动态类型注册表

我们一直假设在编译时就能知道完整的类型列表,并可将其编码到类型标识符对应表中(示例中是通过 switch 语句实现的)。对于整个程序范围而言,前一个要求是不可避免的:由于每个构造函数的调用在某处都是显式的,可以构造的类型总数在编译时已知。然而,我们的解决方案比这更受限制 —— 有一个在工厂方法中硬编码的所有类型的列表。如果不同时将新派生类添加到工厂中,就无法创建派生类。有时,这种限制并不像看起来那么糟糕 —— 例如,游戏中的建筑物列表可能不会经常更改,即使更改了,也必须手动更新完整列表,以生成正确的菜单,使图片和声音出现在正确的位置等。

尽管如此,分层设计的优点之一是,可以在不修改为操作该层次结构而编写的代码的情况下,稍后添加派生类。新的虚函数只需插入到现有的控制流中,即可提供必要的定制行为。可以为工厂构造函数实现同样的想法。

首先,每个派生类必须能够负责自身的构造。这是必要的,正如已经了解到的,对构造函数的显式调用必须在某个地方编写。如果不在通用代码中,就必须是创建新派生类时所添加代码的一部分。例如,可以有一个静态工厂函数:

class Forge : public Building {
public:
  static Building* MakeBuilding() { return new Forge; }
};

其次,类型的列表必须能够在运行时进行扩展,而不是在编译时就固定下来。我们仍然可以使用枚举,但每次添加新的派生类时都必须进行更新。或者,可以在运行时为每个派生类分配一个整数标识符,并确保这些标识符是唯一的。无论采用哪种方式,都需要一个将这些标识符映射到工厂函数的机制,而且这个映射机制不能是 switch 语句或在编译时就固定的内容。该映射必须是可扩展的。可以为此使用 std::map,但如果类型标识符是整数,也可以使用一个以类型标识符为索引的函数指针 std::vector:

class Building;
using BuildingFactory = Building*(*)();
std::vector<BuildingFactory> building_registry;

要注册一个新类型,只需生成一个新的标识符,并将对应的工厂函数添加到该vector中:

size_t building_type_count = 0;
void RegisterBuilding(BuildingFactory factory) {
  building_registry.push_back(factory));
  ++building_type_count;
}

这种注册机制可以封装在基类本身中:

// Example 12
class Building {
  static size_t building_type_count;
  using BuildingFactory = Building* (*)();
  static std::vector<BuildingFactory> registry;

public:
  static size_t (BuildingFactory factory) {
    registry.push_back(factory);
    return building_type_count++;
  }

  static auto MakeBuilding(size_t building_type) {
    BuildingFactory RegisterBuilding factory =
      registry[building_type];
    return std::unique_ptr<Building>(factory());
  }
};

std::vector<Building::BuildingFactory> Building::registry;
size_t Building::building_type_count = 0;

该基类将工厂函数表和已注册派生类型的数量作为静态数据成员,还包含两个静态函数:一个用于注册新类型,另一个用于构造该类注册的某一种类型的对象。注册函数会返回它与工厂函数关联的类型标识符,马上就会用到这个返回值。

现在,只需将每一种新的建筑类型添加到注册表中。这需要分两步完成 —— 首先,需要为每种建筑类添加一个注册方法:

class Forge : public Building {
public:
  static void Register() {
    RegisterBuilding(Forge::MakeBuilding);
  }
};

其次,需要确保在游戏开始前调用所有 Register() 方法,并确保知道每种建筑类型的正确标识符。此时,RegisterBuilding() 函数返回的值就变得至关重要,将把它作为类型标识符存储在类内部:

// Example 12
class Forge : public Building {
public:
  static void Register() {
    RegisterBuilding(Forge::MakeBuilding);
  }
  static const size_t type_tag;
};

const size_t Forge::type_tag =
RegisterBuilding(Forge::MakeBuilding);

注册过程发生在静态变量初始化期间,时间上早于 main() 函数的启动。

工厂函数不一定是静态成员函数:可以通过函数指针调用的内容都可以使用。例如,可以使用不带捕获的 Lambda 表达式:

// Example 12
class Farm : public Building {
public:
  static const size_t type_tag;
};

const size_t Farm::type_tag =
  RegisterBuilding([]()->Building* { return new Farm; });

必须显式指定返回类型,函数指针类型可定义为返回 Building* 且无参数的函数,而 Lambda 表达式在没有强制转换返回值或指定返回类型的情况下,会推导为返回 Farm* 的函数。

现在,调用 Building::MakeBuilding(tag) 将构造一个使用标识符 tag 注册的类型的对象。tag 的值 —— 即类型标识符 —— 作为静态成员存储在每个类中,无需知道其具体值:

std::unique_ptr<Building> farm =
  Building::MakeBuilding(Farm::type_tag);

std::unique_ptr<Building> forge =
  Building::MakeBuilding(Forge::type_tag);

我们的解决方案中,标识符值与类型之间的对应关系直到运行时才确定 —— 程序运行之前,无法知道哪个建筑的 ID 是 5。通常也不需要知道,正确的值会自动存储在每个类中。

这种实现方式与编译器为真正的虚函数生成的代码非常相似 —— 虚函数调用是通过存储在表中的函数指针完成的,这些函数指针通过唯一的标识符(即虚指针)进行访问。主要区别在于,这里的唯一标识符是与每种类型关联的静态数据成员。尽管如此,这已经是能实现的最接近“虚构造函数”的方案了。

这种动态类型注册模式有许多变体,比起在程序启动时生成类型标识符,更倾向于显式指定类型标识符。特别是使用诸如 "farm"(农场)和 "forge"(锻造厂)这样可读性强的名称会很有用。在这种情况下,可以使用std::map<std::string, BuildingFactory> 容器,以字符串作为索引,存储工厂函数指针,而不是使用vector。

另一种修改是允许更通用的可调用对象作为工厂函数,可以使用 std::function,而非函数指针来泛化 BuildingFactory 类型:

using BuildingFactory = std::function<Building*()>;

我们仍然可以将静态工厂方法注册为派生类的工厂,但同时也可以使用 Lambda 表达式和自定义的函数对象:

// Example 13
class Forge : public Building {
public:
  static const size_t type_tag;
};

class ForgeFactory {
public:
  Building* operator()() const { return new Forge; }
};

const size_t Forge::type_tag =
RegisterBuilding(ForgeFactory{});

这些动态工厂的实现,无论是使用函数指针还是更通用的 std::function。要构造的具体对象类型被嵌入到函数或函数对象的代码中,而这些函数或对象的声明却并未提及这些具体类型。这使得我们能够将这些函数存储在单一的函数表或映射中,第6章中讨论的其他类型擦除实现也同样可以应用于此。

简单起见,工厂方法没有使用参数。然而,前一节中已经探讨了传递参数的选项。由于可变参数模板与函数指针配合使用效果不佳(必须预先声明工厂函数的签名),因此传递参数最可能采用的模式是使用参数规格对象:

// Example 14
struct BuildingSpec {};
class Building {
  ...
  using BuildingFactory =
    Building* (*)(const BuildingSpec&);

  static auto MakeBuilding(size_t building_type, const BuildingSpec& spec) {
    BuildingFactory factory = registry[building_type];
    return std::unique_ptr<Building>(factory(spec));
  }
};

struct FarmSpec : public BuildingSpec {
  bool with_pasture {};
  int number_of_stalls {};
  FarmSpec() = default;

  FarmSpec& SetPasture(bool with_pasture) {
    this->with_pasture = with_pasture;
    return *this;
  }

  FarmSpec& SetStalls(int number_of_stalls) {
    this->number_of_stalls = number_of_stalls;
    return *this;
  }
};

class Farm : public Building {
public:
  explicit Farm(const FarmSpec& spec);
  ...
};

const size_t Farm::type_tag = RegisterBuilding(
  [](const BuildingSpec& spec)->Building* {
    return new Farm(static_cast<const FarmSpec&>(spec));
  });

struct ForgeSpec : public BuildingSpec { ... };
class Forge : public Building { ... };

std::unique_ptr<Building> farm =
  Building::MakeBuilding(FarmSpec()
                        .SetPasture(true)
                        .SetStalls(2));

std::unique_ptr<Building> forge =
  Building::MakeBuilding(ForgeSpec()
                        .SetMagic(false)
                        .SetApprentices(4));

目前,我们讨论的所有工厂构造器中,关于创建哪个对象的决策都由程序的外部输入驱动,并且创建过程都由同一个工厂方法完成(可能通过委托给派生类来实现)。现在,将看到工厂模式的一种不同变体,它用于应对一个略有不同的场景。

13.3.5 多态工厂

考虑一个略有不同的问题 —— 在我们的游戏中,每座建筑都会生产某种类型的单位,且单位的类型与建筑的类型唯一对应。城堡招募骑士,巫师塔训练法师,蜘蛛巢穴则产出巨型蜘蛛。现在,通用代码不仅要创建在运行时选定类型的建筑,还要创建类型在编译时未知的新单位。我们已经有了建筑工厂,也可以用类似的方式实现单位工厂,即每个建筑都关联唯一的单位标识符。但这种设计会将单位与建筑之间的对应关系暴露给程序的其他部分,而这其实是不必要的 —— 每座建筑自己知道如何生产正确的单位,程序的其他部分没必要也知道。

这个设计挑战需要一种略有不同的工厂模式 —— 工厂方法确定要创建一个单位,但具体创建哪种单位则由建筑自身决定。这正是模板方法模式与工厂模式的结合应用 —— 整体设计是工厂模式,但具体的单位类型由派生类来定制:

// Example 15
class Unit {};
class Knight : public Unit { ... };
class Mage : public Unit { ... };
class Spider : public Unit { ... };

class Building {
public:
  virtual Unit* MakeUnit() const = 0;
};

class Castle : public Building {
public:
  Knight* MakeUnit() const { return new Knight; }
};

class Tower : public Building {
public:
  Mage* MakeUnit() const { return new Mage; }
};

class Mound : public Building {
public:
  Spider* MakeUnit() const { return new Spider; }
};

每座建筑都有一个用于创建对应单位的工厂,可以通过基类 Building 来访问这些工厂方法:

std::vector<std::unique_ptr<Building>> buildings;
std::vector<std::unique_ptr<Unit>> units;
for (const auto& b : buildings) {
  units.emplace_back(b->MakeUnit());
}

通过基类中的虚函数(通常是纯虚函数)访问、利用多态性的工厂,称为抽象工厂模式。

本例中未展示建筑自身的工厂方法 —— 单位工厂可以与建筑工厂实现共存(本章附带的源代码示例使用了示例12中的建筑工厂)。用于从建筑创建单位的通用代码只需编写一次,当新增建筑和单位的派生类时,无需修改该代码。

所有 MakeUnit() 函数的返回类型各不相同,但它们都是对同一个基类虚函数 Building::MakeUnit() 的重写。这称为协变返回类型 —— 重写方法的返回类型可以是重写方法返回类型的派生类。在我们的例子中,返回类型与类类型相匹配,但这并非必需。基类和派生类都可以作为协变类型使用,即使来自不同的继承层次。然而,只有这样的类型才能成为协变类型,除此之外,重写方法的返回类型必须与基类的虚函数返回类型完全匹配。

当尝试让工厂返回除原始指针以外的其他类型时,严格的协变返回类型规则会带来一些问题。例如,希望返回 std::unique_ptr 而非原始指针。但与 Unit* 和 Knight* 不同,std::unique_ptr<Unit> 和 std::unique_ptr<Knight> 并不是协变类型,不能用作虚函数及其重写方法的返回类型。

我们将在下一节中探讨这些问题,以及其他几个与工厂方法相关的 C++ 特定问题的解决方案。