在C ++中有一件事让我很长时间以来一直感到不舒服,因为尽管听起来很简单,但老实说我不知道​​该怎么做:

我如何实现Factory目标是否正确?

目标:使客户端可以使用工厂方法而不是对象的构造函数实例化某些对象,而不会产生不可接受的后果和性能损失。

“工厂方法模式”是指对象内部的静态工厂方法或在另一个类中定义的方法,或全局函数。通常只是“将类X的常规实例化方法重定向到构造函数之外的其他任何地方的概念”。

让我略过我想到的一些可能的答案。


0)不要制造工厂,要构造函数。

这听起来不错(实际上通常是最好的解决方案),但这不是一般的解决方法。首先,在某些情况下,对象构造是一个任务复杂到足以证明将其提取到另一个类的理由。但是,即使把这个事实放在一边,即使对于仅使用构造函数的简单对象也常常不起作用。

我知道的最简单的示例是二维Vector类。如此简单,但棘手。我希望能够同时从笛卡尔坐标和极坐标构造它。显然,我做不到:

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和位域)进行重载,其类型在语义上是完全不同的(因此-从理论上讲-有效的重载),但实际上却是同样的事情-就像无符号的int或void指针。


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。

无论如何,除了哲学:在一般情况下,我不想强​​迫工厂的用户成为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或引用成员变量,
将参数传递给基类构造函数和成员对象构造函数。

也许还有一些我现在想不出来的缺点,而且我什至没有特别的义务,因为上述要点已经使我信服了。

所以:甚至还没有结束一个实现工厂的良好通用解决方案。


结论:

我们希望有一种对象实例化的方法,它可以:


允许统一实例化而无需分配,
为构造方法赋予不同的有意义的名称(因此不依赖于副参数重载),
不会对性能造成重大影响,并且最好,尤其是在客户端,这是一个严重的代码膨胀问题,
一般来说,可以在任何类中引入。

我相信我已经证明我提到的方法没有不能满足这些要求。

有任何提示吗?请为我提供解决方案,我不想认为这种语言不会允许我正确实现这样一个琐碎的概念。

评论

@Zac,尽管标题非常相似,但实际问题是恕我直言的。

重复的很好,但是这个问题的内容本身很有价值。

提出此问题两年后,我要补充一点:1)这个问题与几种设计模式有关([抽象]工厂,建筑商,请命名,我不喜欢研究其分类法)。 2)这里讨论的实际问题是“如何将对象存储分配与对象构造完全脱钩?”。

@Dennis:仅当您不删除它时。只要调用者获得了指针的所有权(请参阅:负责在适当时删除它),这些方法就完全可以了,只要它是“有文档的”(源代码是文档;-))。
@Boris @Dennis还可以通过返回unique_ptr 而不是T *使其非常明确。

#1 楼


首先,在某些情况下,
对象构造是一个复杂的任务
,足以证明将其提取为另一个类的理由。


我认为这一点是不正确的。复杂性并不重要。相关性是做什么的。如果可以一步构建对象(与构建器模式不同),则构造器是正确的选择。如果您确实需要另一个类来执行此工作,则无论如何它应该是构造函数中使用的辅助类。

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共享类之类的东西来避免复制开销。

关于分配类型,主要原因使用工厂模式通常是多态的。构造函数不能是虚拟的,即使可以,也没有太大意义。使用静态分配或堆栈分配时,您不能以多态方式创建对象,因为编译器需要知道确切的大小。因此,它仅适用于指针和引用。从工厂返回引用也行不通,因为尽管从技术上讲,对象可以通过引用删除,但它可能会造成混乱且容易出错,请参阅返回C ++引用变量的做法是否有害?例如。因此,指针是唯一剩下的东西,它也包括智能指针。换句话说,工厂与动态分配一起使用时最有用,因此您可以执行以下操作:

class Abstract {
  public:
    virtual void do() = 0;
};

class Factory {
  public:
    Abstract *create();
};

Factory f;
Abstract *a = f.create();
a->do();


在其他情况下,工厂只会帮助解决一些小问题,例如您提到的超载问题。如果可以以统一的方式使用它们,那将是很好的选择,但这并没有太大的伤害,因为它可能是不可能的。

评论


笛卡尔和Polar结构为+1。通常最好创建直接表示其预期数据的类和结构(与一般的Vec结构相反)。您的Factory也是一个很好的例子,但是您的例子没有说明谁拥有指针“ a”。如果工厂'f'拥有它,那么它可能会在'f'离开范围时被销毁,但是如果'f'不拥有它,对于开发人员来说,记住释放该内存很重要,否则内存泄漏可能发生。

–大卫·彼得森(David Peterson)
2013年3月2日19:07



当然可以通过引用删除对象!参见stackoverflow.com/a/752699/404734当然,这提出了一个问题,即通过引用返回动态内存是否明智,因为存在潜在地通过拷贝分配返回值的问题(调用者当然也可以做一些事情)就像int a = * returnsAPoninterToInt()一样,然后会遇到相同的问题,如果返回了动态分配的内存,就像引用一样,但是在指针版本中,用户必须显式取消引用,而不仅仅是忘记显式引用,这是错误的。 。

–凯撒路迪
2013年9月12日18:52

@Kaiserludi,很高兴。我没有想到这一点,但这仍然是做事的“邪恶”方式。编辑我的答案以反映这一点。

–谢尔盖·塔切诺夫(Sergei Tachenov)
2013年9月14日下午4:29

如何创建不可变的不同非多态类?那么,工厂模式是否适合在C ++中使用?

–daaxix
2014年3月14日在17:54

@daaxix,为什么需要工厂来创建非多态类的实例?我不知道不变性与这一切有什么关系。

–谢尔盖·塔切诺夫(Sergei Tachenov)
2014年3月15日下午4:09

#2 楼

简单工厂示例:
// 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();
    }
};


评论


@LokiAstari因为使用智能指针是释放对内存的控制的最简单方法。与其他语言相比,已知对C / C ++ lang的控制是至高无上的,从中获得最大的优势。更不用说智能指针会产生类似于其他托管语言的内存开销这一事实。如果您想要自动内存管理的便利,请开始使用Java或C#进行编程,但不要将这种混乱引入C / C ++。

–luke1985
14年4月19日在21:07

@ lukasz1985该示例中的unique_ptr没有性能开销。与其他任何语言相比,管理包括内存在内的资源都是C ++的最大优势之一,因为您可以在不损失性能的情况下确定性地做到这一点,而又不会失去控制权,但是您却恰恰相反。有些人不喜欢C ++隐式执行的操作,例如通过智能指针进行内存管理,但是如果要使所有内容强制显式显示,请使用C;否则,请使用C。权衡问题要少几个数量级。我认为您不赞成一个好的建议是不公平的。

–TheCppZoo
2015年6月12日23:47



@EdMaster:我以前没有回应,因为他显然是在拖钓。请不要喂巨魔。

–马丁·约克
2015年6月14日在2:03

@LokiAstari他可能是一个巨魔,但他说的话可能会使人们感到困惑

–TheCppZoo
15年6月14日在19:12

@yau:是的。但是:boost :: ptr_vector <>效率更高一点,因为它知道它拥有指针,而不是将工作委托给子类。但是boost :: ptr_vector <>的主要优点是它通过引用(而不是指针)公开其成员,因此它很容易与标准库中的算法一起使用。

–马丁·约克
16年5月5日在17:32

#3 楼

您是否考虑过完全不使用工厂,而是更好地使用类型系统?我可以想到两种不同的方法来执行这种操作:

选项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) { /* ... */ }
};


您可以这样写: br />
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);


其中在使您为每个构造函数拥有唯一的原型的同时,也很好而富有表现力。

#4 楼

您可以在以下位置阅读一个非常好的解决方案:http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus

最好的解决方案是“评论和讨论”,请参阅“不需要静态的Create方法”。

从这个想法出发,我做了一个工厂。请注意,我使用的是Qt,但是您可以将QMap和QString更改为标准等效项。

#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");


#5 楼

我大多同意接受的答案,但是现有答案中没有涉及C ++ 11选项:


按值返回工厂方法的结果,并且
提供便宜的move构造函数。

示例:

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());


我什么时候可以使用它?

如果在公共构造函数上,如果不进行一些初步计算就不可能为所有类成员提供有意义的初始化程序,那么我可能会将该构造函数转换为静态变量方法。静态方法执行初步计算,然后通过私有构造函数返回值结果,该私有构造函数仅进行成员初始化。

我说“可能”是因为它取决于哪种方法给出最清晰的代码而没有效率低下。

评论


包装OpenGL资源时,我广泛使用了此方法。已删除的副本构造函数和副本分配会强制使用移动语义。然后,我创建了一堆静态工厂方法来创建每种类型的资源。这比OpenGL基于枚举的运行时分派更具可读性,后者通常根据传递的枚举具有一堆冗余函数参数。这是一个非常有用的模式,很奇怪这个答案没有更高。

–问题
18年5月27日在23:04

#6 楼

Loki同时具有工厂方法和抽象工厂。两者均由Andei Alexandrescu在(现代)C ++设计中有大量文献记载。 factory方法可能与您似乎要使用的方法更接近,尽管它仍然有些不同(至少在提供内存的情况下,它要求您在工厂创建该类型的对象之前注册一个类型)。

评论


即使它已经过时了(我对此表示怀疑),它仍然可以完美地使用。我仍然在新的C ++ 14项目中使用基于MC ++ D的Factory,效果很好!此外,Factory和Singleton模式可能是最不合时宜的部分。尽管可以用std :: function和替换Loki之类的Function和类型操作,而lambda,线程,右值引用可能需要一些细微调整,但对于标准的工厂来说,没有标准的替代品。描述它们。

–金属
17年2月3日,19:27



#7 楼

我不会回答所有问题,因为我认为它太广泛了。仅需注意以下几点:


在某些情况下,对象构造是一项任务复杂的任务,足以证明将其提取到另一个类是合理的。


类实际上是一个Builder,而不是Factory。


通常,我不想强​​迫工厂的用户只能进行动态分配。


然后,您可以让工厂将其封装在智能指针中。我相信这样您也可以吃蛋糕。

这也消除了与价值回报相关的问题。


结论:制作通过返回一个对象来实现工厂确实是某些情况下的解决方案(例如前面提到的二维向量),但仍不是构造函数的一般替代方法。


确实。所有设计模式都有其(特定于语言的)约束和缺点。建议仅在它们帮助您解决问题时才使用它们,而不是为了自己。

如果您追求的是“完美”的工厂实现,那么祝您好运。

评论


感谢你的回答!但是您能否解释一下使用智能指针将如何释放动态分配的限制?我不太明白这部分。

–科斯
2011年7月25日在18:13

@Kos,使用智能指针,您可以向用户隐藏实际对象的分配/取消分配。他们只看到封装的智能指针,它指向外界的行为就像一个静态分配的对象。

–彼得·托克(PéterTörök)
2011年7月25日在20:40

@Kos,不是严格意义上的AFAIR。您传入要包装的对象,该对象可能已在某个时刻动态分配。然后,智能指针将拥有它的所有权,并确保在不再需要智能指针时将其正确销毁(对于不同类型的智能指针,时间的决定将有所不同)。

–彼得·托克(PéterTörök)
11年7月26日在7:35

#8 楼

这是我的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();
  }
}


评论


对我来说很好您将如何实现(也许是一些宏魔术)静态注册?试想一下,基类是对象的某种服务类。派生类为这些对象提供了一种特殊的服务。您想通过为每种服务添加从base派生的类来逐步添加各种服务。

– St0fF
19年9月7日在17:58

#9 楼

工厂模式

class Point
{
public:
  static Point Cartesian(double x, double y);
private:
};


如果编译器不支持返回值优化,则放弃它,它可能根本不包含太多优化...

评论


真的可以认为这是工厂模式的实现吗?

–丹尼斯
2013年6月30日20:38在

@丹尼斯:作为一个退化的案例,我会这样认为。 Factory的问题在于它非常通用并且涵盖了很多领域。例如,工厂可以添加参数(取决于环境/设置)或提供某些缓存(与Flyweight / Pool相关),但是这些情况仅在某些情况下才有意义。

– Matthieu M.
13年7月1日在6:27

如果只需要更改编译器,就像听起来那样简单:)

–罗齐纳
2014年3月3日在21:16

@rozina::)在Linux上运行良好(gcc / clang非常兼容);我承认Windows仍然相对封闭,尽管Windows在64位平台上应该会更好(如果我没记错的话,更少的专利)。

– Matthieu M.
2014年3月4日在7:45

然后您便拥有了一些低于标准的编译器的整个嵌入式世界..::)我正在使用一种没有返回值优化的方法。我希望有。不幸的是,目前这不是一个选择。希望将来会进行更新,否则我们将进行其他切换:)

–罗齐纳
2014年5月5日在12:23

#10 楼

我知道这个问题已经在3年前得到了回答,但这可能正是您所要的。

Google在几周前发布了一个库,该库允许轻松灵活地分配动态对象。它在这里:http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html

#11 楼

extern std::pair<std::string_view, Base*(*)()> const factories[2];

decltype(factories) factories{
  {"blah", []() -> Base*{return new Blah;}},
  {"foo", []() -> Base*{return new Foo;}}
};