Должен ли я всегда переходить к аргументам конструктора или сеттера `sink`?

struct TestConstRef {
    std::string str;
    Test(const std::string& mStr) : str{mStr} { }
};

struct TestMove {
    std::string str;
    Test(std::string mStr) : str{std::move(mStr)} { }
};

После просмотра GoingNative 2013 я понял, что аргументы приемника всегда должны передаваться по значению и перемещаться с помощью std::move. Является ли TestMove::ctor правильным способом применения этой идиомы? Есть ли случай, когда TestConstRef::ctor лучше/эффективнее?


Как насчет тривиальных сеттеров? Должен ли я использовать следующую идиому или передать const std::string&?

struct TestSetter {
    std::string str;
    void setStr(std::string mStr) { str = std::move(str); }
};

Ответ 1

Простой ответ: да.


Причина довольно проста: если вы храните по стоимости, вам может потребоваться переместить (из временного) или сделать копию (из l-значения). Давайте рассмотрим, что происходит в обеих ситуациях, в обоих направлениях.

Из временного

  • если вы принимаете аргумент const-ref, временный привязан к const-ref и не может быть перенесен из него, поэтому вы в конечном итоге делаете (бесполезную) копию.
  • если вы принимаете аргумент по значению, значение инициализируется из временного (перемещение), а затем вы сами переходите от аргумента, поэтому копия не создается.

Одно ограничение: класс без эффективного конструктора move (например, std::array<T, N>), потому что тогда вы сделали две копии вместо одной.

Из l-значения (или const временно, но кто будет это делать...)

  • если вы принимаете аргумент const-ref, там ничего не происходит, а затем вы копируете его (не можете перейти от него), таким образом создается одна копия.
  • если вы принимаете аргумент по значению, вы копируете его в аргументе и затем переходите от него, таким образом создается одна копия.

Одно ограничение: те же... классы, для которых перемещение сродни копированию.

Итак, простой ответ заключается в том, что в большинстве случаев, используя раковину, вы избегаете ненужных копий (заменяя их перемещением).

Единственное ограничение - это классы, для которых конструктор перемещения является таким же дорогостоящим (или почти таким же дорогим), как и конструктор копирования; в этом случае, имея два хода вместо одной копии, это "худшее". К счастью, такие классы редки (массивы - это один случай).

Ответ 2

Немного поздно, так как этот вопрос уже имеет принятый ответ, но в любом случае... вот альтернатива:

struct Test {
    std::string str;
    Test(std::string&& mStr) : str{std::move(mStr)} { } // 1
    Test(const std::string& mStr) : str{mStr} { } // 2
};

Почему это было бы лучше? Рассмотрим два случая:

Из временного (case // 1)

Для str вызывается только один конструктор move.

Из l-значения (случай // 2)

Для str вызывается только один экземпляр-конструктор.

Вероятно, это не может быть лучше.

Но подождите, есть еще:

Никакой дополнительный код не генерируется на стороне вызывающего абонента! Вызов конструктора copy или move (который может быть встроен или нет) теперь может работать в реализации вызываемой функции (здесь: Test::Test), и поэтому требуется только одна копия этого кода. Если вы используете передачу параметров по-значению, вызывающий отвечает за создание объекта, который передается функции. Это может складываться в больших проектах, и я стараюсь избегать его, если это возможно.