【问题标题】:How to implement the factory method pattern in C++ correctly如何在 C++ 中正确实现工厂方法模式
【发布时间】:2011-07-04 11:52:05
【问题描述】:

在 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或引用成员变量,
  • 将参数传递给基类构造函数和成员对象构造函数。

可能还有一些我现在想不出的缺点,而且我什至不觉得特别有义务,因为上面的要点已经说服了我。

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


结论:

我们希望有一种对象实例化方式:

  • 无论分配如何,都允许统一实例化,
  • 为构造方法提供不同的、有意义的名称(因此不依赖于参数重载),
  • 不会引入显着的性能损失,最好是显着的代码膨胀损失,尤其是在客户端,
  • 笼统,如:可以为任何类引入。

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

有什么提示吗?请给我一个解决方案,我不想认为这种语言不允许我正确实现这样一个微不足道的概念。

【问题讨论】:

  • @Zac,虽然标题很相似,但实际问题恕我直言不同。
  • 很好的副本,但 this 问题的文字本身就很有价值。
  • 问这个问题两年后,我有几点要补充:1) 这个问题与几种设计模式([抽象]工厂,建造者,你说出来,我不喜欢深入研究他们的分类)。 2) 这里讨论的实际问题是“如何将对象存储分配与对象构造完全分离?”。
  • @Dennis:除非你不delete它。这些方法非常好,只要它是“记录的”(源代码是文档;-))调用者拥有指针的所有权(阅读:负责在适当的时候删除它)。
  • @Boris @Dennis 你也可以通过返回 unique_ptr&lt;T&gt; 而不是 T* 来使其非常明确。

标签: c++ design-patterns idioms factory-method


【解决方案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'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();

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

【讨论】:

  • +1 用于笛卡尔和极坐标结构。通常最好创建直接表示它们要用于的数据的类和结构(与一般的 Vec 结构相反)。你的工厂也是一个很好的例子,但你的例子并没有说明谁拥有指针'a'。如果工厂 'f' 拥有它,那么它可能会在 'f' 离开作用域时被销毁,但如果 'f' 不拥有它,开发人员必须记住释放该内存,否则内存泄漏可能发生。
  • 当然可以通过引用删除对象!请参阅stackoverflow.com/a/752699/404734 这当然会引发一个问题,如果通过引用返回动态内存是否明智,因为可能会通过复制分配返回值的问题(调用者当然也可以执行类似 int a = *returnsAPoninterToInt () 然后会面临同样的问题,如果动态地返回所有涂层内存,就像引用一样,但在指针版本中,用户必须显式取消引用而不是忘记显式引用,这是错误的)。
  • @Kaiserludi,好点子。我没有想到这一点,但这仍然是一种“邪恶”的做事方式。编辑了我的答案以反映这一点。
  • 创建不同的不可变的非多态类怎么样?那么工厂模式是否适合在 C++ 中使用?
  • @daaxix,为什么需要工厂来创建非多态类的实例?我不明白不变性与这有什么关系。
【解决方案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++ 语言被认为是至高无上的,并从中获得最大的优势。更不用说智能指针会产生类似于其他托管语言的内存开销这一事实。如果您想要自动内存管理的便利性,请开始使用 Java 或 C# 进行编程,但不要将这些乱七八糟的东西放入 C/C++ 中。
  • @lukasz1985 unique_ptr 在该示例中没有性能开销。管理包括内存在内的资源是 C++ 相对于任何其他语言的最大优势之一,因为您可以在没有性能损失和确定性的情况下做到这一点,而不会失去控制,但您说的恰恰相反。有些人不喜欢 C++ 隐式所做的事情,比如通过智能指针进行内存管理,但如果你想要让所有内容都必须明确,请使用 C;权衡是问题少了几个数量级。我认为你拒绝一个好的建议是不公平的。
  • @EdMaster:我之前没有回复,因为他显然是在拖钓。请不要喂巨魔。
  • @LokiAstari 他可能是个巨魔,但他说的话可能会让人们感到困惑
  • @yau:是的。但是:boost::ptr_vector&lt;&gt; 效率更高一点,因为它知道它拥有指针,而不是将工作委托给子类。 但是boost::ptr_vector&lt;&gt; 的主要优点是它通过引用(而不是指针)公开其成员,因此它非常容易与标准库中的算法一起使用。
【解决方案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) { /* ... */ }
};

这让您可以编写如下内容:

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阅读一个很好的解决方案

    最好的解决方案是在“cmets 和讨论”上,参见“无需静态创建方法”。

    根据这个想法,我做了一个工厂。请注意,我使用的是 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");
    

    【讨论】:

      【解决方案5】:

      我基本同意接受的答案,但现有答案中没有涵盖 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());
      

      我什么时候可以使用这个?

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

      我说'可能',因为这取决于哪种方法可以提供最清晰的代码,而不会不必要地低效。

      【讨论】:

      • 我在包装 OpenGL 资源时广泛使用了它。删除了复制构造函数和复制赋值,强制使用移动语义。然后我创建了一堆静态工厂方法来创建每种类型的资源。这比 OpenGL 基于枚举的运行时调度更具可读性,后者通常具有一堆冗余函数参数,具体取决于传递的枚举。这是一个非常有用的模式,很惊讶这个答案没有更高。
      【解决方案6】:

      Loki 同时拥有Factory MethodAbstract Factory。两者都在 Andei Alexandrescu 的 Modern C++ Design 中(广泛地)记录在案。工厂方法可能更接近您所追求的,尽管它仍然有点不同(至少如果内存可用,它需要您在工厂创建该类型的对象之前注册一个类型)。

      【讨论】:

      • 即使它已经过时(我对此表示怀疑),它仍然可以正常使用。我仍然在一个新的 C++14 项目中使用基于 MC++D 的工厂,效果很好!此外,Factory 和 Singleton 模式可能是最不过时的部分。虽然像 Function 这样的 Loki 片段和类型操作可以用 std::function&lt;type_traits&gt; 替换,而 lambdas、线程、右值 refs 的含义可能需要一些小的调整,但没有标准的替代工厂的单例,因为他描述了他们。
      【解决方案7】:

      我不会尝试回答我的所有问题,因为我认为它太宽泛了。只是几个注意事项:

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

      那个类实际上是一个Builder,而不是一个工厂。

      在一般情况下,我不想强​​制工厂的用户被限制动态分配。

      然后你可以让你的工厂将它封装在一个智能指针中。我相信这样你就可以吃蛋糕了。

      这也消除了与按值返回相关的问题。

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

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

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

      【讨论】:

      • 感谢您的回答!但是你能解释一下使用智能指针如何释放动态分配的限制吗?我不太明白这部分。
      • @Kos,使用智能指针,您可以向用户隐藏实际对象的分配/释放。他们只看到封装的智能指针,它在外界看来就像一个静态分配的对象。
      • @Kos,不是严格意义上的 AFAIR。您传入要包装的对象,您可能在某些时候动态分配了该对象。然后智能指针获得它的所有权,并确保在不再需要它时正确销毁它(不同类型的智能指针的时间决定不同)。
      【解决方案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 派生的类来逐步添加不同类型的服务。
      【解决方案9】:

      工厂模式

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

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

      【讨论】:

      • 这真的可以被认为是工厂模式的实现吗?
      • @Dennis:作为一个退化的案例,我会这么认为。 Factory 的问题在于它非常通用并且涵盖了很多领域。例如,工厂可以添加参数(取决于环境/设置)或提供一些缓存(与享元/池相关),但这些情况仅在某些情况下才有意义。
      • 如果只是改变编译器会像你说的那么简单:)
      • @rozina: :) 它在 Linux 中运行良好(gcc/clang 非常兼容);我承认 Windows 仍然相对封闭,尽管它在 64 位平台上应该会变得更好(如果我没记错的话,专利会更少)。
      • 然后你就拥有了整个嵌入式世界和一些低于标准的编译器.. :) 我正在使用一个没有返回值优化的类似编译器。我希望它有。不幸的是,此时切换它不是一种选择。希望将来它会更新,或者我们会为其他东西做一个切换:)
      【解决方案10】:

      我知道这个问题已在 3 年前得到解答,但这可能是您正在寻找的。​​p>

      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;}}
        };
        

        【讨论】:

          猜你喜欢
          • 2012-05-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多