Здравствуйте, Skorodum, Вы писали:
S>А можете привести пример кода, который вы могли бы у себя улучшить при поддержке mandatory copy/move elision? Подозреваю, что ручная оптимизация возвращаемой из функций памяти будет не нужна.
Mandatory copy/move elision здорово облегчают написание и улучшают качество разного рода создателей "сложных" объектов — парсеров, загрузчиков, фабрик и пр. Давай условно будем называвать такую процедуру "загрузчик". "Сложные" объекты, это, в первую очередь, такие объекты, которые имеют сложные связи с внешним миром и эти связи хочется заложить прямо на этапе конструирования объекта, чтобы исключить даже саму возможность существования невалидных объектов, даже временно. Также для таких объектов очень часто хочется запретить операции копирования/перемещения, а также конструктор по умолчанию. Также к сложным объектам можно отнести большие объекты с дорогой и сложной операцией копирования. Благодаря возможности mandatory copy/move elision загрузчик имеет возможность подготовить все необходимые данные, возможно, предварительно что-то подгрузить из DB, поискать все необходимые связи в графе объектов и т.п. и, наконец, создать сам объект и вернуть его по значению — даже при запрещенных конструкторах копирования и перемещения. Что при этом очень важно — загрузчик не навязывает внешнему миру способ владения объеком — верхняя логика сама решает, будет ли она владеть объектом через какой-то смарт-поинтер и через какой именно, либо просто создаст автоматический объект на стеке (в юнит-тесте, например).
Здравствуйте, rg45, Вы писали:
R>Mandatory copy/move elision здорово облегчают написание и улучшают качество разного рода создателей "сложных" объектов — парсеров, загрузчиков, фабрик и пр.
S>..единственный способ создать объект ComplexObject это вызов createComplexObject и никак иначе? И при этом никакого копирования памяти?
Да, все верно.
Единственное, что хочется заметить, что совсем не обязательно закрывать прямо все конструкторы в классе. Вполне возможен вариант, что какой-то конструкор остается открытым и доступным, просто его использование может быть сопряжено с какими-то более-менее сложными сопуствующими операциями — вычитка дополнительных данных из базы, поиск связей с другими объектами и пр. Поэтому функция-загрузчик может предоставляться скорее в качестве помощника, берущего на себя все сопутствующие заботы, а не в качестве полицая, запрещающего прямое создание.
S>>..единственный способ создать объект ComplexObject это вызов createComplexObject и никак иначе? И при этом никакого копирования памяти?
R>Да, все верно.
R>Единственное, что хочется заметить, что совсем не обязательно закрывать прямо все конструкторы в классе. Вполне возможен вариант, что какой-то конструкор остается открытым и доступным, просто его использование может быть сопряжено с какими-то более-менее сложными сопуствующими операциями — вычитка дополнительных данных из базы, поиск связей с другими объектами и пр. Поэтому функция-загрузчик может предоставляться скорее в качестве помощника, берущего на себя все сопутствующие заботы, а не в качестве полицая, запрещающего прямое создание.
Чтоб приблизить пример к более практическому применению, можно вспомнить про сериализацию:
class MyDeserializer
{
public:
ComplexObject DeserializeComplexObject();
}
При этом подразумевается, что и сам класс MyDeserializer может в себе хранить какие-то данные, которые используются для создания объекта — сам поток данных, из которого идет вычитка, карты объектов графа для построения связей и пр.
Ну и понятное дело, все эти фабрики, загрузчики, десериализаторы могут быть шаблонными в той или иной мере, мы здесь об этом не вспоминаем просто, чтоб сконцентрироваться на главном вопросе.
Спасибо. А как происходит обработка ошибок в фабрике? Там же доступны только исключения и тогда чем это отличается от того же самого прямо в конструкторе класса?
S>ComplexObject createComplexObject()
{
// как сообщаем об ошибках здесь? Только исключения
...
return ComplexObject(0);
}
Здравствуйте, Skorodum, Вы писали:
S>Спасибо. А как происходит обработка ошибок в фабрике? Там же доступны только исключения и тогда чем это отличается от того же самого прямо в конструкторе класса? S>
S>>ComplexObject createComplexObject()
S>{
S> // как сообщаем об ошибках здесь? Только исключения
S> ...
S> return ComplexObject(0);
S>}
S>
Логически фабрику можно рассматривать как некое расширение конструктора. Соответственно и стратегия обработки ошибок такая же, как в конструкторах — исключения.
Здравствуйте, rg45, Вы писали:
R>Логически фабрику можно рассматривать как некое расширение конструктора. Соответственно и стратегия обработки ошибок такая же, как в конструкторах — исключения.
Наверное, еще std::optional можно использовать.
Здравствуйте, Skorodum, Вы писали:
R>>Логически фабрику можно рассматривать как некое расширение конструктора. Соответственно и стратегия обработки ошибок такая же, как в конструкторах — исключения. S>Наверное, еще std::optional можно использовать.
Не-не-не, это по факту будет равносильно возврату к старой схеме со смарт-поинтерами. Смотрите, какие возможности предоставляет фабрика с mandatory copy/move elision:
#include <iostream>
#include <string>
#include <vector>
#include <memory>
struct Foo
{
const int id;
explicit Foo(int id) : id(id) {}
Foo() = delete;
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
////////////////////////////////////////////////////////////////////////////////////////////////
Foo(Foo&&) = delete; // перемещение не обязательно запрещать явно
Foo& operator=(Foo&&) = delete; // оно автоматически запрещается, когда мы запрещаем копированиеvoid test() const { std::cout << "Foo: " << id << std::endl; }
};
Foo make_foo()
{
static int last_id {};
return Foo(++last_id);
}
int main()
{
auto foo = make_foo();
const auto uptr = std::unique_ptr<Foo>(new Foo(make_foo()));
const auto sptr = std::shared_ptr<Foo>(new Foo(make_foo()));
foo.test();
uptr->test();
sptr->test();
}
То есть, мы можем выбирать стратегию владения объектом за пределами фабрики — вот, что важно. Это может быть любой тип смарпоинтера, а может быть просто объект на стеке с автоматическим временем жизни. Если же изменить тип результата make_foo на std::optional<Foo>, все эти возможности тут же пропадут.
Здравствуйте, rg45, Вы писали:
R>Если же изменить тип результата make_foo на std::optional<Foo>, все эти возможности тут же пропадут.
Понял: мы же не может Foo в optional копировать. Спасибо.
Здравствуйте, Skorodum, Вы писали:
R>>Если же изменить тип результата make_foo на std::optional<Foo>, все эти возможности тут же пропадут. S>Понял: мы же не может Foo в optional копировать. Спасибо.
Да, именно. Если резюмировать, то mandatory copy/move elision можно рассматривать как возможность расширения конструкторов. Сам конструктор содержит в себе только те действия, которые свойственны непосредственно классу объекта. А над этим мы можем еще построить массу разнообразных фабрик/загрузчиков/парсеров и т.п., в которых будут выполняться действия по созданию объекта, специфичные для мира, в котором этот объект создается. И в итоге всегда будем иметь под рукой простую и удобную процедуру создания объекта.