Преимущества передачи по значению и std :: move по сравнению с передачей по ссылке


108

В данный момент я изучаю 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}?

3
При передаче по ссылке Creature c("John");делает дополнительную копию
user253751 07

1
Эта ссылка может быть полезной для чтения, она также касается прохождения std::string_viewи SSO.
lubgr 07

Я обнаружил, clang-tidyчто это отличный способ увлечься ненужными микрооптимизациями за счет удобочитаемости. Прежде всего, стоит задать вопрос: сколько раз мы действительно вызываем Creatureконструктор.
cz

Ответы:


39
  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 ++, а также этот вопрос .


В этом есть смысл, особенно потому, что это делает декларацию более интуитивно понятной для чтения. Я не уверен, что полностью понимаю часть вашего ответа, касающуюся освобождения (и понимаю связанный поток), поэтому просто чтобы проверить, использую ли я move, пространство будет освобождено. Если я не использую move, он освобождается только в том случае, если выделенное пространство слишком мало для хранения новой строки, что приводит к повышению производительности. Это правильно?
Blackbot

1
Да, именно так. При назначении m_nameиз const std::string&параметра внутренняя память повторно используется до тех пор, пока она m_nameумещается. При назначении перемещения m_nameпамять должна быть предварительно освобождена. В противном случае «украсть» ресурсы из правой части задания было невозможно.
lubgr 06

Когда он становится висящей ссылкой? Я думаю, что список инициализации использует глубокую копию.
Ли Тайцзи

108
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • Прошел именующие связывается с name, затем скопированной в m_name.

  • Прошло RValue связывается с name, затем скопированной в m_name.


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • Передаются именующим будет скопирован в 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)} { }
  • Прошел именующие связывается с 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 стремится упростить это с помощью Concepts .


В C ++ 17 на prvalue влияет гарантированное исключение копий , которое, если применимо, уменьшит количество копий / перемещений при передаче аргументов функциям.


Для (1) регистр pr-value и xvalue не идентичны, так как c ++ 17 нет?
Oliv

1
Учтите, что в этом случае SFINAE не нужен для идеального форварда. Это нужно только для устранения неоднозначности. Это правдоподобно полезно для потенциальных сообщений об ошибках при передаче плохих аргументов
Caleth

@Oliv Да. xvalues ​​нужно переместить, а prvalues ​​можно убрать :)
Rakete1111,

1
Можем ли мы написать: Creature(const std::string &name) : m_name{std::move(name)} { }в (2) ?
Skytree

4
@skytree: вы не можете перейти от константного объекта, так как перемещение изменяет источник. Это будет компилироваться, но будет создана копия.
Витторио Ромео

1

То, как вы проходите, здесь не единственная переменная, то , что вы передаете, имеет большое значение между ними.

В C ++, у нас есть все виды стоимостных категорий , и это «идиома» существует для случаев , когда вы проходите в RValue (например , как "Alex-string-literal-that-constructs-temporary-std::string"или std::move(nameString)), что приводит к 0 копий в std::stringделаются (тип , даже не должен быть копией конструктивна для аргументов rvalue) и использует только std::stringконструктор перемещения.

В некоторой степени связанные вопросы и ответы .


1

У метода передачи по значению и перемещения по сравнению с передачей по ссылке (rv) есть несколько недостатков:

  • вызывает создание 3 объектов вместо 2;
  • передача объекта по значению может привести к дополнительным накладным расходам стека, поскольку даже обычный строковый класс обычно как минимум в 3 или 4 раза больше, чем указатель;
  • создание объектов аргументов будет производиться на стороне вызывающего абонента, что приведет к раздуванию кода;

Не могли бы вы прояснить, почему это вызывает появление 3 объектов? Насколько я понимаю, я могу просто передать «Питер» как строку. Это будет порождаться, копироваться, а затем перемещаться, не так ли? И разве стек не будет использоваться в какой-то момент? Не в момент вызова конструктора, а в той m_name{name}части, где он копируется?
Blackbot

@Blackbot Я имел в виду ваш пример: std::string nameString("Alex"); Creature c(nameString);один объект nameString, другой - аргумент функции, а третий - поле класса.
user7860670 06

0

В моем случае переключение на передачу по значению, а затем выполнение std: move вызвало ошибку использования кучи после освобождения в Address Sanitizer.

https://travis-ci.org/github/acgetchell/CDT-plusplus/jobs/679520360#L3165

Итак, я отключил его, как и предложение в clang-tidy.

https://github.com/acgetchell/CDT-plusplus/compare/80c96789f0a2...0d78fd63b332

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.