Должен ли я всегда переходить к аргументам конструктора или сеттера `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
), и поэтому требуется только одна копия этого кода. Если вы используете передачу параметров по-значению, вызывающий отвечает за создание объекта, который передается функции. Это может складываться в больших проектах, и я стараюсь избегать его, если это возможно.