Преимущества pass-by-value и std :: move over-reference
Я изучаю C++ на данный момент и стараюсь избегать появления вредных привычек. Из того, что я понимаю, clang-tidy содержит много "лучших практик", и я стараюсь придерживаться их как можно лучше (хотя я не всегда понимаю, почему они считаются хорошими), но я не уверен, если я понять, что рекомендуется здесь.
Я использовал этот класс из учебника:
class Creature
{
private:
std::string m_name;
public:
Creature(const std::string &name)
: m_name{name}
{
}
};
Это приводит к предположению от clang-tidy, что я должен передавать значение вместо ссылки и использовать std::move
. Если я делаю, я получаю предложение сделать name
ссылки (для того чтобы он не копируется каждый раз) и предупреждение, что std::move
не будет иметь никакого эффекта, потому что name
является const
, поэтому я должен удалить его.
Единственный способ, которым я не получаю предупреждение, - это удалить const
полностью:
Creature(std::string name)
: m_name{std::move(name)}
{
}
Это кажется логичным, поскольку единственное преимущество const
состояло в том, чтобы предотвратить испорченность исходной строки (чего не происходит, потому что я прошел по значению). Но я читал на CPlusPlus.com:
Хотя обратите внимание, что -in стандартное library- перемещение подразумевает, что перемещенный объект остается в действительном, но неуказанном состоянии. Это означает, что после такой операции значение перемещенного объекта должно быть уничтожено или назначено новое значение; доступ к нему в противном случае дает неопределенное значение.
Теперь представьте этот код:
std::string nameString("Alex");
Creature c(nameString);
Поскольку nameString
передается по значению, std::move
будет только отменять name
внутри конструктора и не трогать исходную строку. Но каковы преимущества этого? Кажется, что контент копируется только один раз - если я m_name{name}
ссылку, когда я вызываю m_name{name}
, если я m_name{name}
по значению при его передаче (а затем он будет перемещен). Я понимаю, что это лучше, чем передача по значению и не использование std::move
(потому что он дважды копируется).
Итак, два вопроса:
- Правильно ли я понял, что здесь происходит?
- Есть ли потенциал для использования
std::move
over m_name{name}
by reference и просто вызов m_name{name}
?
Ответы
Ответ 1
- Правильно ли я понял, что здесь происходит?
Да.
- Есть ли потенциал для использования
std::move
over m_name{name}
by reference и просто вызов m_name{name}
?
Легко понять функциональную подпись без каких-либо дополнительных перегрузок. Подпись немедленно показывает, что аргумент будет скопирован - это избавит вызывающих абонентов от интереса, может ли const std::string&
reference быть сохранена в качестве члена данных, возможно, позже будет обвисшей ссылкой. И нет необходимости перегружать std::string&& name
и const std::string&
arguments, чтобы избежать ненужных копий, когда rvalues передаются функции. Передача lvalue
std::string nameString("Alex");
Creature c(nameString);
к функции, которая принимает свой аргумент по значению, вызывает одну копию и одну конструкцию движения. Передача значения r на ту же функцию
std::string nameString("Alex");
Creature c(std::move(nameString));
вызывает две движущиеся конструкции. Напротив, когда параметр функции const std::string&
, всегда будет копия, даже если передается аргумент rvalue. Это явно преимущество, если тип аргумента дешев для перемещения-построения (это имеет место для std::string
).
Но есть недостаток, который следует учитывать: аргументация не работает для функций, которые присваивают аргумент функции другой переменной (вместо ее инициализации):
void setName(std::string name)
{
m_name = std::move(name);
}
приведет к освобождению ресурса, к m_name
относится имя m_name
до его переназначения. Я рекомендую прочитать пункт 41 в "Эффективном современном" C++, а также этот вопрос.
Ответ 2
/* (0) */
Creature(const std::string &name) : m_name{name} { }
-
m_name
lvalue связывается с name
, затем копируется в m_name
.
-
Переданное rvalue связывается с name
, затем копируется в m_name
.
/* (1) */
Creature(std::string name) : m_name{std::move(name)} { }
-
m_name
lvalue копируется в name
, а затем перемещается в m_name
.
-
Полученное rvalue перемещается в name
, а затем перемещается в m_name
.
/* (2) */
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
-
m_name
lvalue связывается с name
, затем копируется в m_name
.
-
rname
rvalue связывается с rname
, затем перемещается в m_name
.
Поскольку операции перемещения обычно быстрее, чем копии, (1) лучше, чем (0), если вы пропускаете много временных рядов. (2) является оптимальным с точки зрения копирования/перемещения, но требует повторения кода.
Повторения кода можно избежать с совершенной пересылкой:
/* (3) */
template <typename T,
std::enable_if_t<
std::is_convertible_v<std::remove_cvref_t<T>, std::string>,
int> = 0
>
Creature(T&& name) : m_name{std::forward<T>(name)} { }
Возможно, вы захотите ограничить T
, чтобы ограничить домен типов, с которым может быть создан этот конструктор (как показано выше). С++ 20 стремится упростить это с помощью концепций.
В С++ 17 на prvalues влияет гарантированное копирование, которое, если применимо, уменьшает количество копий/перемещений при передаче аргументов функциям.
Ответ 3
Как вы проходите, это не единственная переменная здесь, то, что вы проходите, делает большую разницу между ними.
В C++ у нас есть все виды категорий значений, и эта "идиома" существует для случаев, когда вы передаете rvalue (например, "Alex-string-literal-that-constructs-temporary-std::string"
или std::move(nameString)
), в результате чего получается 0 копий std::string
(тип даже не должен быть конструктивным для аргументов rvalue) и использует только конструктор std::string
move.
Несколько связанных вопросов и ответов.
Ответ 4
Существует несколько недостатков подхода pass-by-value-and-move по ссылке pass-by- (rv):
- он вызывает появление 3 объектов вместо 2;
- передача объекта по значению может привести к дополнительным накладным расходам, потому что даже обычный класс строк обычно не менее 3 или 4 раза больше, чем указатель;
- построение объектов аргументов будет выполняться на стороне вызывающего абонента, вызывая раздувание кода;