Преимущества передачи по значению и std::двигаться через перевал по ссылке
я изучаю 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:
хотя обратите внимание, что-в стандартной библиотеке - перемещение подразумевает, что перемещенный объект остается в допустимом, но неопределенном состоянии. Это означает, что после такой операции значение перемещенного объекта должно быть только уничтожено или присвоено новое значение; доступ к нему в противном случае дает неопределенное значение.
теперь представьте себе это код:
std::string nameString("Alex");
Creature c(nameString);
, потому что nameString
передается по значению, std::move
только аннулирует name
внутри конструктора и не трогать исходную строку. Но каковы преимущества этого? Похоже, что содержимое копируется только один раз в любом случае - если я передаю ссылку, когда я звоню m_name{name}
, если я передаю значение, когда я передаю его (а затем он перемещается). Я понимаю, что это лучше, чем передавать значение и не использовать std::move
(потому что он копируется дважды.)
два вопроса:
- правильно ли я понял, что здесь происходит?
- есть ли какие-либо преимущества использования
std::move
за прохождение по ссылке и просто вызовm_name{name}
?
4 ответа:
- правильно ли я понял, что здесь происходит?
да.
- есть ли какие-либо преимущества использования
std::move
за прохождение по ссылке и просто вызовm_name{name}
?легко понять сигнатуру функции без каких-либо дополнительных перегрузок. Подпись сразу же показывает, что аргумент будет скопирован - это избавляет абонентов от необходимости задаваться вопросом, является ли
const std::string&
ссылка может быть сохранена как элемент данных, возможно, став висящей ссылкой позже. И нет необходимости перегружать наstd::string&& name
иconst std::string&
аргументы, чтобы избежать ненужных копий, когда rvalues передаются функции. Передача lvaluestd::string nameString("Alex"); Creature c(nameString);
к функции, которая принимает свой аргумент по значению, приводит одна копия и одна конструкция перемещения. Передача значения rvalue в ту же функцию
std::string nameString("Alex"); Creature c(std::move(nameString));
вызывает две конструкции перемещения. В отличие от этого, когда функция параметр
const std::string&
, всегда будет копия, даже при передаче аргумента rvalue. Это явно преимущество до тех пор, пока тип аргумента дешев для move-construct (это относится кstd::string
).но есть и обратная сторона: рассуждение не работает для функций, которые назначают аргумент функции другой переменной (вместо того, чтобы инициализировать его):
void setName(std::string name) { m_name = std::move(name); }
приведет к освобождению ресурса, который
m_name
относится к тому, прежде чем это переназначенный. Я рекомендую прочитать пункт 41 в эффективном современном C++ , а также этот вопрос.
/* (0) */ Creature(const std::string &name) : m_name{name} { }
прошел lvalue привязывается к
name
, потом скопировал наm_name
.прошел rvalue привязывается к
name
, потом скопировал наm_name
.
/* (1) */ Creature(std::string name) : m_name{std::move(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)} { }
прошел lvalue привязывается к
name
, потом скопировал наm_name
.прошел 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
для того, чтобы ограничить домен типов, с помощью которых можно создать экземпляр этого конструктора (как показано выше). C++20 стремится упростить это с помощью концепции.
В C++17, prvalues влияет гарантированная копия elision, что-если применимо-уменьшит количество копий / перемещений при передаче аргументов в функции.
как вы проходите-не единственная переменная здесь что вы проходите делает большую разницу между ними.
В C++, у нас есть все виды категорий стоимостью и эта "идиома" существует для случаев, когда вы проходите в rvalue (например,
"Alex-string-literal-that-constructs-temporary-std::string"
илиstd::move(nameString)
), что приводит к 0 копий наstd::string
создается (тип даже не должен быть копируемым для Аргументов rvalue) и использует толькоstd::string
конструктор перемещения.
есть несколько недостатков, передаваемых по значению и двигаться подход через перевал-к-(РВ)ссылка:
- это вызывает 3 объекта, которые будут порождены вместо 2;
- передача объекта по значению может привести к дополнительным накладным расходам стека, потому что даже обычный класс строк обычно по крайней мере в 3 или 4 раза больше указателя;
- построение объектов аргумента будет выполняться на стороне вызывающего абонента, вызывая раздувание кода;