C++ 中有一件事情让我很长时间以来都感到不舒服,因为我真的不知道该怎么做,尽管这听起来很简单:
如何在 C++ 中正确实现工厂方法?
目标:允许客户端使用工厂方法而不是对象的构造函数来实例化某些对象,而不会产生不可接受的后果和性能损失。
“工厂方法模式”是指对象内部的静态工厂方法或在另一个类中定义的方法或全局函数。只是一般“将类 X 的常规实例化方式重定向到构造函数以外的任何地方的概念”。
让我浏览一下我想到的一些可能的答案。
0)不要制造工厂,制造构造器。
这听起来不错(实际上通常是最好的解决方案),但不是一般的补救措施。首先,在某些情况下,对象构造是一项复杂到足以证明将其提取到另一个类的任务。但是即使把这个事实放在一边,即使对于简单的对象来说,只使用构造函数通常也行不通。
我知道的最简单的例子是二维向量类。如此简单,却又很棘手。我希望能够从笛卡尔坐标和极坐标中构建它。显然,我不能这样做:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
我的自然思维方式是:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
哪个,而不是构造函数,导致我使用静态工厂方法......这本质上意味着我正在以某种方式实现工厂模式(“类成为它自己的工厂”)。这看起来不错(并且适合这种特殊情况),但在某些情况下会失败,我将在第 2 点中进行描述。请继续阅读。
另一种情况:尝试通过某些 API 的两个不透明 typedef 重载(例如不相关域的 GUID,或 GUID 和位域),语义上完全不同的类型(因此 - 在理论上 - 有效重载)但实际上结果是同样的事情——比如无符号整数或空指针。
1)Java方式
Java 很简单,因为我们只有动态分配的对象。制造工厂很简单:
class FooFactory {
public Foo createFooInSomeWay() {
// can be a static method as well,
// if we don't need the factory to provide its own object semantics
// and just serve as a group of methods
return new Foo(some, args);
}
}
在 C++ 中,这转换为:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
凉爽的?确实,经常。但是,这迫使用户只使用动态分配。静态分配使 C++ 变得复杂,但也常常使它变得强大。另外,我相信存在一些不允许动态分配的目标(关键字:嵌入式)。这并不意味着这些平台的用户喜欢编写干净的 OOP。
无论如何,抛开哲学:在一般情况下,我不想强迫工厂的用户被限制为动态分配。
2) 按值返回
好的,所以我们知道 1) 在我们想要动态分配时很酷。为什么我们不在此之上添加静态分配?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
什么?我们不能通过返回类型重载?哦,我们当然不能。因此,让我们更改方法名称以反映这一点。是的,我编写了上面的无效代码示例只是为了强调我多么不喜欢更改方法名称的需要,例如因为我们现在无法正确实现与语言无关的工厂设计,因为我们必须更改名称 - 和此代码的每个用户都需要记住实现与规范的差异。
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
好的......我们有它。这很难看,因为我们需要更改方法名称。这是不完美的,因为我们需要编写两次相同的代码。但是一旦完成,它就会起作用。正确的?
嗯,通常。但有时它不会。在创建 Foo 时,实际上是依赖编译器为我们做返回值优化,因为 C++ 标准已经足够仁慈了,编译器厂商没有指定对象何时就地创建,何时复制返回C++ 中按值的临时对象。因此,如果 Foo 的复制成本很高,那么这种方法是有风险的。
如果 Foo 根本不可复制怎么办?嗯,呵呵。 (请注意,在保证复制省略的 C++17 中,对于上面的代码,不可复制不再是问题)
结论:通过返回对象来制造工厂确实是某些情况下的解决方案(例如前面提到的二维向量),但仍然不是构造函数的一般替代品。
3)两期建设
有人可能会想出的另一件事是将对象分配和初始化的问题分开。这通常会导致如下代码:
class Foo {
public:
Foo() {
// empty or almost empty
}
// ...
};
class FooFactory {
public:
void createFooInSomeWay(Foo& foo, some, args);
};
void clientCode() {
Foo staticFoo;
auto_ptr<Foo> dynamicFoo = new Foo();
FooFactory factory;
factory.createFooInSomeWay(&staticFoo);
factory.createFooInSomeWay(&dynamicFoo.get());
// ...
}
人们可能认为它就像一种魅力。我们在代码中付出的唯一代价......
既然我已经写了所有这些并把它作为最后一个,我也必须不喜欢它。 :) 为什么?
首先……我是真心不喜欢两期建设的概念,用起来有愧疚感。如果我用“如果它存在,它处于有效状态”的断言来设计我的对象,我会觉得我的代码更安全,更不容易出错。我喜欢这样。
不得不放弃那个约定并改变我的对象的设计只是为了制造它的工厂......好吧,笨拙。
我知道以上不会说服很多人,所以让我给出一些更扎实的论据。使用两阶段构造,您不能:
初始化 const 或引用成员变量,
将参数传递给基类构造函数和成员对象构造函数。
可能还有一些我现在想不出的缺点,而且我什至不觉得特别有义务,因为上述要点已经说服了我。
所以:甚至没有一个很好的通用解决方案来实现工厂。
结论:
我们希望有一种对象实例化的方式,它会:
无论分配如何,都允许统一实例化,
为构造方法赋予不同的、有意义的名称(因此不依赖于参数重载),
不会引入显着的性能损失,最好是显着的代码膨胀损失,尤其是在客户端,
是一般的,如:可能被引入任何类。
我相信我已经证明我提到的方法不能满足这些要求。
有什么提示吗?请给我一个解决方案,我不想认为这种语言不允许我正确实现这样一个微不足道的概念。
delete
它时。这些方法非常好,只要它是“记录的”(源代码是文档;-))调用者拥有指针的所有权(阅读:负责在适当的时候删除它)。
unique_ptr<T>
而不是 T*
来使其非常明确。
首先,在某些情况下,对象构造是一项复杂到足以证明将其提取到另一个类的任务。
我认为这一点是不正确的。复杂性并不重要。相关性是什么。如果一个对象可以一步构建(不像在构建器模式中),那么构造器就是正确的地方。如果你真的需要另一个类来执行这项工作,那么它应该是一个辅助类,无论如何都可以从构造函数中使用。
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
有一个简单的解决方法:
struct Cartesian {
inline Cartesian(float x, float y): x(x), y(y) {}
float x, y;
};
struct Polar {
inline Polar(float angle, float magnitude): angle(angle), magnitude(magnitude) {}
float angle, magnitude;
};
Vec2(const Cartesian &cartesian);
Vec2(const Polar &polar);
唯一的缺点是它看起来有点冗长:
Vec2 v2(Vec2::Cartesian(3.0f, 4.0f));
但好在你可以立即看到你正在使用的坐标类型,同时你不必担心复制。如果您想要复制,而且成本高昂(当然,正如分析所证明的那样),您可能希望使用 Qt's shared classes 之类的东西来避免复制开销。
至于分配类型,使用工厂模式的主要原因通常是多态性。构造函数不能是虚拟的,即使可以,也没有多大意义。使用静态或堆栈分配时,您不能以多态方式创建对象,因为编译器需要知道确切的大小。所以它只适用于指针和引用。从工厂返回引用也不起作用,因为虽然从技术上讲,对象可以通过引用删除,但它可能相当混乱且容易出错,例如 Is the practice of returning a C++ reference variable, evil?。所以指针是唯一剩下的东西,这也包括智能指针。换句话说,工厂在与动态分配一起使用时最有用,因此您可以执行以下操作:
class Abstract {
public:
virtual void do() = 0;
};
class Factory {
public:
Abstract *create();
};
Factory f;
Abstract *a = f.create();
a->do();
在其他情况下,工厂只是帮助解决您提到的过载问题。如果可以以统一的方式使用它们会很好,但它可能是不可能的并没有太大的伤害。
简单工厂示例:
// Factory returns object and ownership
// Caller responsible for deletion.
#include <memory>
class FactoryReleaseOwnership{
public:
std::unique_ptr<Foo> createFooInSomeWay(){
return std::unique_ptr<Foo>(new Foo(some, args));
}
};
// Factory retains object ownership
// Thus returning a reference.
#include <boost/ptr_container/ptr_vector.hpp>
class FactoryRetainOwnership{
boost::ptr_vector<Foo> myFoo;
public:
Foo& createFooInSomeWay(){
// Must take care that factory last longer than all references.
// Could make myFoo static so it last as long as the application.
myFoo.push_back(new Foo(some, args));
return myFoo.back();
}
};
unique_ptr
没有性能开销。管理包括内存在内的资源是 C++ 相对于任何其他语言的最大优势之一,因为您可以在没有性能损失和确定性的情况下做到这一点,而不会失去控制,但您说的恰恰相反。有些人不喜欢 C++ 隐式做的事情,比如通过智能指针进行内存管理,但如果你想要让所有内容都强制显式,请使用 C;权衡是问题少了几个数量级。我认为你拒绝一个好的建议是不公平的。
boost::ptr_vector<>
更高效一点,因为它知道它拥有指针,而不是将工作委托给子类。 但是 boost::ptr_vector<>
的主要优点是它通过引用(而不是指针)公开其成员,因此它很容易与标准库中的算法一起使用。
你有没有想过完全不使用工厂,而是很好地使用类型系统?我可以想到两种不同的方法来做这种事情:
选项1:
struct linear {
linear(float x, float y) : x_(x), y_(y){}
float x_;
float y_;
};
struct polar {
polar(float angle, float magnitude) : angle_(angle), magnitude_(magnitude) {}
float angle_;
float magnitude_;
};
struct Vec2 {
explicit Vec2(const linear &l) { /* ... */ }
explicit Vec2(const polar &p) { /* ... */ }
};
这使您可以编写以下内容:
Vec2 v(linear(1.0, 2.0));
选项 2:
您可以像 STL 对迭代器等使用“标签”。例如:
struct linear_coord_tag linear_coord {}; // declare type and a global
struct polar_coord_tag polar_coord {};
struct Vec2 {
Vec2(float x, float y, const linear_coord_tag &) { /* ... */ }
Vec2(float angle, float magnitude, const polar_coord_tag &) { /* ... */ }
};
第二种方法使您可以编写如下所示的代码:
Vec2 v(1.0, 2.0, linear_coord);
这也很好并且富有表现力,同时允许您为每个构造函数拥有独特的原型。
您可以在以下位置阅读一个非常好的解决方案:http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus
最好的解决方案是关于“评论和讨论”,请参阅“无需静态创建方法”。
从这个想法,我做了一个工厂。请注意,我使用的是 Qt,但您可以将 QMap 和 QString 更改为 std 等效项。
#ifndef FACTORY_H
#define FACTORY_H
#include <QMap>
#include <QString>
template <typename T>
class Factory
{
public:
template <typename TDerived>
void registerType(QString name)
{
static_assert(std::is_base_of<T, TDerived>::value, "Factory::registerType doesn't accept this type because doesn't derive from base class");
_createFuncs[name] = &createFunc<TDerived>;
}
T* create(QString name) {
typename QMap<QString,PCreateFunc>::const_iterator it = _createFuncs.find(name);
if (it != _createFuncs.end()) {
return it.value()();
}
return nullptr;
}
private:
template <typename TDerived>
static T* createFunc()
{
return new TDerived();
}
typedef T* (*PCreateFunc)();
QMap<QString,PCreateFunc> _createFuncs;
};
#endif // FACTORY_H
示例用法:
Factory<BaseClass> f;
f.registerType<Descendant1>("Descendant1");
f.registerType<Descendant2>("Descendant2");
Descendant1* d1 = static_cast<Descendant1*>(f.create("Descendant1"));
Descendant2* d2 = static_cast<Descendant2*>(f.create("Descendant2"));
BaseClass *b1 = f.create("Descendant1");
BaseClass *b2 = f.create("Descendant2");
我大多同意接受的答案,但现有答案中没有涵盖 C++11 选项:
按值返回工厂方法结果,以及
提供一个廉价的移动构造函数。
例子:
struct sandwich {
// Factory methods.
static sandwich ham();
static sandwich spam();
// Move constructor.
sandwich(sandwich &&);
// etc.
};
然后就可以在栈上构造对象了:
sandwich mine{sandwich::ham()};
作为其他事物的子对象:
auto lunch = std::make_pair(sandwich::spam(), apple{});
或动态分配:
auto ptr = std::make_shared<sandwich>(sandwich::ham());
我什么时候可以使用这个?
如果在公共构造函数上,如果没有一些初步计算就不可能为所有类成员提供有意义的初始化器,那么我可能会将该构造函数转换为静态方法。静态方法执行初步计算,然后通过私有构造函数返回一个值结果,该构造函数只进行成员初始化。
我说“可能”是因为它取决于哪种方法可以提供最清晰的代码,而不会不必要地低效。
Loki 有一个 Factory Method 和一个 Abstract Factory。两者都在 Andei Alexandrescu 的 Modern C++ Design 中(广泛地)记录在案。工厂方法可能更接近您所追求的,尽管它仍然有点不同(至少如果内存服务,它需要您在工厂创建该类型的对象之前注册一个类型)。
Function
这样的 Loki 部分和类型操作可以用 std::function
和 <type_traits>
替换,而 lambdas、线程、右值 refs 的含义可能需要一些小的调整,但没有标准的替代工厂的单例,因为他描述了他们。
我不会试图回答我所有的问题,因为我认为它太宽泛了。只是几个注意事项:
在某些情况下,对象构造是一项复杂到足以证明将其提取到另一个类的任务。
该类实际上是 Builder,而不是工厂。
在一般情况下,我不想强制工厂的用户被限制为动态分配。
然后你可以让你的工厂将它封装在一个智能指针中。我相信这样你也可以吃蛋糕。
这也消除了与按值返回有关的问题。
结论:通过返回对象来制造工厂确实是某些情况下的解决方案(例如前面提到的二维向量),但仍然不是构造函数的一般替代品。
的确。所有设计模式都有其(特定于语言的)约束和缺点。建议仅在它们帮助您解决问题时使用它们,而不是为了它们自己。
如果你在“完美”的工厂实施之后,那么,祝你好运。
这是我的 c++11 风格的解决方案。参数“base”是所有子类的基类。创建者是用于创建子类实例的 std::function 对象,可能是对您的子类的静态成员函数“create(some args)”的绑定。这可能并不完美,但对我有用。这是一种“通用”的解决方案。
template <class base, class... params> class factory {
public:
factory() {}
factory(const factory &) = delete;
factory &operator=(const factory &) = delete;
auto create(const std::string name, params... args) {
auto key = your_hash_func(name.c_str(), name.size());
return std::move(create(key, args...));
}
auto create(key_t key, params... args) {
std::unique_ptr<base> obj{creators_[key](args...)};
return obj;
}
void register_creator(const std::string name,
std::function<base *(params...)> &&creator) {
auto key = your_hash_func(name.c_str(), name.size());
creators_[key] = std::move(creator);
}
protected:
std::unordered_map<key_t, std::function<base *(params...)>> creators_;
};
使用示例。
class base {
public:
base(int val) : val_(val) {}
virtual ~base() { std::cout << "base destroyed\n"; }
protected:
int val_ = 0;
};
class foo : public base {
public:
foo(int val) : base(val) { std::cout << "foo " << val << " \n"; }
static foo *create(int val) { return new foo(val); }
virtual ~foo() { std::cout << "foo destroyed\n"; }
};
class bar : public base {
public:
bar(int val) : base(val) { std::cout << "bar " << val << "\n"; }
static bar *create(int val) { return new bar(val); }
virtual ~bar() { std::cout << "bar destroyed\n"; }
};
int main() {
common::factory<base, int> factory;
auto foo_creator = std::bind(&foo::create, std::placeholders::_1);
auto bar_creator = std::bind(&bar::create, std::placeholders::_1);
factory.register_creator("foo", foo_creator);
factory.register_creator("bar", bar_creator);
{
auto foo_obj = std::move(factory.create("foo", 80));
foo_obj.reset();
}
{
auto bar_obj = std::move(factory.create("bar", 90));
bar_obj.reset();
}
}
工厂模式
class Point
{
public:
static Point Cartesian(double x, double y);
private:
};
如果你的编译器不支持返回值优化,放弃它,它可能根本不包含太多优化......
Factory
的问题在于它非常通用并且涵盖了很多领域。例如,工厂可以添加参数(取决于环境/设置)或提供一些缓存(与享元/池相关),但这些情况仅在某些情况下才有意义。
extern std::pair<std::string_view, Base*(*)()> const factories[2];
decltype(factories) factories{
{"blah", []() -> Base*{return new Blah;}},
{"foo", []() -> Base*{return new Foo;}}
};
我知道这个问题已在 3 年前得到回答,但这可能是您正在寻找的。
谷歌几周前发布了一个库,允许简单灵活的动态对象分配。这是:http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html