Преимущества передачи по значению и 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 (потому что он копируется дважды.)

два вопроса:

  1. правильно ли я понял, что здесь происходит?
  2. есть ли какие-либо преимущества использования std::move за прохождение по ссылке и просто вызов m_name{name}?
4 53
c++

4 ответа:

  1. правильно ли я понял, что здесь происходит?

да.

  1. есть ли какие-либо преимущества использования std::move за прохождение по ссылке и просто вызов m_name{name}?

легко понять сигнатуру функции без каких-либо дополнительных перегрузок. Подпись сразу же показывает, что аргумент будет скопирован - это избавляет абонентов от необходимости задаваться вопросом, является ли const std::string& ссылка может быть сохранена как элемент данных, возможно, став висящей ссылкой позже. И нет необходимости перегружать на std::string&& name и const std::string& аргументы, чтобы избежать ненужных копий, когда rvalues передаются функции. Передача lvalue

std::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 раза больше указателя;
  • построение объектов аргумента будет выполняться на стороне вызывающего абонента, вызывая раздувание кода;