【问题标题】:C++ static factory method vs constructor: how to avoid copying?C ++静态工厂方法与构造函数:如何避免复制?
【发布时间】:2018-11-29 05:12:12
【问题描述】:

This question 要求以简洁的方式在 C++ 中实现静态工厂方法,this answer 描述了一种清晰的方式来实现这一点。返回值优化将使我们免于制作Object 的不必要副本,从而使这种创建Object 的方式与直接调用构造函数一样有效。在私有构造函数中将i 复制到id 的开销可以忽略不计,因为它是一个小的int

但是,当Object 包含一个实例变量是类Foo 的实例(需要复杂的初始化逻辑)而不是一个小的原始类型时,问题和答案并未涵盖更复杂的情况。假设我想使用传递给Object 的参数构造Foo。使用构造函数的解决方案如下所示:

class Object {
    Foo foo;

public:
    Object(const FooArg& fooArg) {
        // Create foo using fooArg here
        foo = ...
    }
}

在我看来,与引用的答案类似的静态工厂方法的替代方法是:

class Object {
    Foo foo;

    explicit Object(const Foo& foo_):
        foo(foo_)
    {

    }

public:
    static Object FromFooArg(const FooArg& fooArg) {
        // Create foo using fooArg here
        Foo foo = ...
        return Object(foo);
    }
}

在这里,将foo_ 复制到foo 的开销不再一定可以忽略不计,因为Foo 可以是任意复杂的类。此外,据我了解(这里是 C++ 新手,所以我可能错了),这段代码隐含地要求为 Foo 定义一个复制构造函数。

在这种情况下,什么是实现这种模式的类似干净但有效的方法?

为了预测关于为什么这是相关的可能问题,我考虑让构造函数具有比仅仅复制参数更复杂的逻辑作为反模式。我希望构造函数:

  • 保证工作正常,不抛出异常,
  • 不要在后台进行繁重的计算。

因此,我更喜欢将复杂的初始化逻辑放入静态方法中。此外,这种方法提供了额外的好处,例如即使输入参数类型相同,也可以通过静态工厂方法名称进行重载,并且可以清楚地以方法名称说明内部正在执行的操作。

【问题讨论】:

  • 从 C++11 开始,您确实可以使用 移动语义。但这是一个复杂的话题;最好的答案是像 Stroustrup 这样的好书。写得很好的问题; +1。
  • 严格来说,您使用公共 c'tor 的第一个版本也很昂贵。您默认初始化foo(我假设所有初始化都很昂贵),然后您创建另一个 Foo 对象,并将其分配给默认构造的对象。
  • @StoryTeller 嗯,我不知道一定是这样。我认为在第一个版本中,如果我执行foo = Foo(fooArg),那么只会调用该构造函数并且不会发生复制。我对 C++ 的了解是从 Java 开始的。无论如何,我主要关注的是 copying 而不是 initialization (即使对于潜在的大对象也可能很便宜,例如空数组初始化)。
  • @Vossler - 我建议在这里小心行事。 C++ 对象模型与 Java 非常不同。成员的初始化和复制不像在 Java 中那样不连贯。
  • 直接从fooArg初始化foo 的构造函数有什么问题?当然,这样的构造函数也可以被工厂细读。

标签: c++ oop


【解决方案1】:

感谢移动构造函数,您可能会这样做:

class Object {
    Foo foo;

    explicit Object(Foo&& foo_) : foo(std::move(foo_)) {}

public:
    static Object FromFooArg(const FooArg& fooArg) {
        // Create foo using fooArg here
        Foo foo = ...
        return Object(std::move(foo));
    }
};

如果Foo 不可移动,则可以将其包装在智能指针中:

class Object {
    std::unique_ptr<Foo> foo;

    explicit Object(std::unique_ptr<Foo>&& foo_) : foo(std::move(foo_)) {}

public:
    static Object FromFooArg(const FooArg& fooArg) {
        // Create foo using fooArg here
        std::unique_ptr<Foo> foo = ...
        return Object(std::move(foo));
    }
};

【讨论】:

  • 谢谢!我可能偏向于这些解决方案 b/c 这或多或少地模仿了我更熟悉的 Java pass-by-reference 风格。关于此的两个问题:1)更接近的 Java 克隆将在解决方案 2 中使用 std::shared_ptr 而不是 std::unique_ptr,对吧?然后我们有点模仿 Java 的引用计数垃圾收集? 2)虽然这感觉不错,但这种简单操作的冗长和复杂性让人感觉不舒服。如果我通常遵循这个习语,那么我将不得不将所有内容包装成智能指针,对吗? 我在 C++ 中做错了吗
  • 好吧,C++ 不是 Java。在所有地方使用shared_ptr 会产生成本,并且也很难推断确定性破坏。
  • @Vossler:垃圾收集!= 引用计数,但是使用 shared_ptr 会更像 Java。
  • 如果您有copy elision 为您完成这项工作,为什么还需要堆分配?如果Foo 的构建成本很高,您只需支付一次费用。
【解决方案2】:

直接从所需的参数初始化构造函数中的实例有什么问题?

class Object
{
    Foo foo;                         // or const Foo foo, disallowing assignment

public:

    explicit Object(FooCtorArgs const&fooArg,
                    const AdditionalData*data = nullptr)
      : foo(fooArg)                  // construct instance foo directly from args
    {
        foo.post_construction(data); // optional; doesn't work with const foo
    }

    static Object FromFooArg(FooCtorArgs const&fooArg,
                             const AdditionalData*data = nullptr)
    { 
        return Object{fooArg,data};  // copy avoided by return value optimization
    }
};

AFAICT,无需复制/移动任何内容,即使您需要调整foo 后期构建。

【讨论】:

  • 虽然这对我有用,但这仅适用于 Foo 直接来自 FooArg 的构造函数的特定情况(对吗?...)。相反,如果在FromFooArg 中我默认初始化foo 并使用fooArg 的一些计算(或一般情况下的其他参数)手动填充它,那么您将不得不回退到我原来的sn-p。此外,这种解决方案有点违反我不让构造函数执行复杂初始化逻辑的动机(这取决于FooArg 也遵守此约定)。
  • 这种担忧是转移注意力。在这种情况下,您可以在Object 的工厂或构造函数中填写Object::foo 实例,请参阅编辑。然而,一个设计良好的class Foo 不应该需要这样的构造后初始化。
  • @ÖöTiib 请阅读我的原始问题 - 文本和代码均未表明我假设 Foo 具有直接来自 FooArg 的构造函数。如果您认为原始问题的表述不正确,那么您有理由拒绝它,但是您暗示我对这个答案的评论对Foo 施加了一些临时约束是错误的。
  • @Vossler 还能怎样?您的Object 只能从FooArg 构造,并且具有Foo 组件,因此这清楚地暗示Foo 也只能从FooArg 构造。否则,信息确实会从一些未说明的依赖项中横向流动。
  • @ÖöTiib Foo 确实可以从 FooArg 构造,但不必定义直接构造函数。谁说我可以控制Foo 的实现?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-07-13
  • 2014-02-18
  • 1970-01-01
  • 2013-07-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多