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

Итак, два вопроса:

  1. Правильно ли я понял, что здесь происходит?
  2. Есть ли потенциал для использования std::move over m_name{name} by reference и просто вызов m_name{name}?

Ответ 1

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

Да.

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