Чтобы понять, почему это хороший шаблон, мы должны изучить альтернативы как в C ++ 03, так и в C ++ 11.
У нас есть метод C ++ 03 для получения std::string const&:
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
в этом случае всегда будет выполняться одна копия. Если вы строите из необработанной строки C, std::stringбудет построено, а затем снова скопировано: два распределения.
Существует метод C ++ 03, позволяющий взять ссылку на a std::string, а затем заменить ее на локальную std::string:
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
это версия C ++ 03 «семантики перемещения», и swapее часто можно оптимизировать, чтобы сделать ее очень дешевой (во многом как a move). Это также следует анализировать в контексте:
S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
и заставляет вас сформировать невременное std::string, а затем отбросить его. (Временное std::stringне может быть привязано к неконстантной ссылке). Однако выполняется только одно распределение. Версия C ++ 11 примет a &&и потребует, чтобы вы вызывали ее с помощью std::moveили с помощью временного: для этого требуется, чтобы вызывающий объект явно создал копию вне вызова и переместил эту копию в функцию или конструктор.
struct S
{
std::string data;
S(std::string&& str): data(std::move(str))
{}
};
Использование:
S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal
Затем мы можем сделать полную версию C ++ 11, которая поддерживает как копирование, так и move:
struct S
{
std::string data;
S(std::string const& str) : data(str) {} // lvalue const, copy
S(std::string && str) : data(std::move(str)) {} // rvalue, move
};
Затем мы можем изучить, как это используется:
S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data
std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data
std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data
Совершенно очевидно, что этот метод двух перегрузок по крайней мере так же эффективен, если не больше, чем два вышеуказанных стиля C ++ 03. Я назову эту версию с двумя перегрузками «самой оптимальной» версией.
Теперь мы рассмотрим версию с дублированием:
struct S2 {
std::string data;
S2( std::string arg ):data(std::move(x)) {}
};
в каждом из этих сценариев:
S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data
std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data
std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data
Если сравнить эту параллельную версию с «самой оптимальной» версией, мы сделаем ровно одну дополнительную move! Ни разу не делаем лишнего copy.
Так что, если предположить, что moveэто дешево, эта версия дает нам почти такую же производительность, что и наиболее оптимальная версия, но в 2 раза меньше кода.
И если вы берете, скажем, от 2 до 10 аргументов, сокращение кода будет экспоненциальным - в 2 раза меньше с 1 аргументом, в 4 раза с 2, 8x с 3, 16x с 4, 1024x с 10 аргументами.
Теперь мы можем обойти это с помощью идеальной пересылки и SFINAE, позволяя вам написать единственный конструктор или шаблон функции, который принимает 10 аргументов, выполняет SFINAE, чтобы гарантировать, что аргументы имеют соответствующие типы, а затем перемещает или копирует их в местное государство по мере необходимости. Хотя это предотвращает тысячукратное увеличение размера программы, из этого шаблона все же может быть сгенерирована целая куча функций. (экземпляры шаблонных функций генерируют функции)
А большое количество сгенерированных функций означает больший размер исполняемого кода, что само по себе может снизить производительность.
За несколько moveсекунд мы получаем более короткий код и почти такую же производительность, а зачастую и более простой для понимания код.
Теперь это работает только потому, что мы знаем, что при вызове функции (в данном случае конструктора) нам потребуется локальная копия этого аргумента. Идея в том, что если мы знаем, что собираемся делать копию, мы должны сообщить вызывающей стороне, что мы делаем копию, поместив ее в наш список аргументов. Затем они могут оптимизировать тот факт, что они собираются дать нам копию (например, перейдя к нашему аргументу).
Еще одно преимущество техники «принимать по значению» состоит в том, что конструкторы перемещения часто не являются исключениями. Это означает, что функции, которые принимают значение по значению и выходят из своего аргумента, часто могут быть без исключения, перемещая любые throws из своего тела в вызывающую область. (кто может избежать этого через прямое построение иногда или создавать элементы и moveв аргумент, чтобы контролировать, где происходит выброс). Часто стоит сделать методы nothrow.