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