С++ static factory method vs constructor: как избежать копирования?

Этот вопрос требует чистого способа реализации статического заводского метода в C++, и этот ответ описывает четкий способ сделать это. Оптимизация возвращаемого значения спасла бы нас от создания ненужной копии Object, тем самым создав такой способ создания Object так же эффективно, как непосредственный вызов конструктора. Накладные расходы на копирование i в id внутри частного конструктора незначительны, потому что это небольшой int.

Однако вопрос и ответ не охватывают более сложный случай, когда Object содержит переменную экземпляра, которая является экземпляром класса Foo (который требует сложной логики инициализации), а не малым примитивным типом. Предположим, что я хочу построить Foo используя аргументы, переданные Object. Решение с использованием конструктора будет выглядеть примерно так:

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.

Что было бы таким же чистым, но и эффективным способом реализации этой модели в этом случае?

Чтобы предвидеть возможные вопросы о том, почему это уместно, я считаю, что конструкторы с логикой сложнее, чем просто копирование аргументов как анти-шаблон. Я ожидаю, что конструктор:

  • гарантированно работать, а не бросать исключения,
  • и не делайте тяжелых расчетов под капотом.

Таким образом, я предпочитаю ставить сложную логику инициализации в статические методы. Более того, такой подход обеспечивает дополнительные преимущества, такие как перегрузка по имени статического заводского метода, даже если типы входных аргументов одинаковы, и возможность четко указывать, что делается внутри имени метода.

Ответы

Ответ 1

Что случилось с инициализацией экземпляра в конструкторе непосредственно из аргументов, необходимых для этого?

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.

Ответ 2

Благодаря конструктору перемещения вы можете:

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