Чтобы понять, почему это хороший шаблон, мы должны изучить альтернативы как в 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
секунд мы получаем более короткий код и почти такую же производительность, а зачастую и более простой для понимания код.
Теперь это работает только потому, что мы знаем, что при вызове функции (в данном случае конструктора) нам потребуется локальная копия этого аргумента. Идея в том, что если мы знаем, что собираемся делать копию, мы должны сообщить вызывающей стороне, что мы делаем копию, поместив ее в наш список аргументов. Затем они могут оптимизировать тот факт, что они собираются дать нам копию (например, перейдя к нашему аргументу).
Еще одно преимущество техники «принимать по значению» состоит в том, что конструкторы перемещения часто не являются исключениями. Это означает, что функции, которые принимают значение по значению и выходят из своего аргумента, часто могут быть без исключения, перемещая любые throw
s из своего тела в вызывающую область. (кто может избежать этого через прямое построение иногда или создавать элементы и move
в аргумент, чтобы контролировать, где происходит выброс). Часто стоит сделать методы nothrow.