Является ли pass-by-value разумным дефолтом в С++ 11?
В традиционном С++ переход по значениям в функции и методы замедляется для больших объектов и, как правило, неодобрен. Вместо этого программисты на C++ склонны передавать ссылки, что происходит быстрее, но в них вводятся всевозможные сложные вопросы, связанные с владением, и особенно в области управления памятью (в том случае, если объект выделен в виде кучи)
Теперь, в С++ 11, мы имеем ссылки Rvalue и перемещаем конструкторы, а это означает, что можно реализовать большой объект (например, std::vector
), который дешево передавать по значению в и из функции.
Итак, означает ли это, что значение по умолчанию должно быть передано по значению для экземпляров типов, таких как std::vector
и std::string
? Как насчет пользовательских объектов? Какая новая лучшая практика?
Ответы
Ответ 1
Это разумное значение по умолчанию, если вам нужно сделать копию внутри тела. Это то, о чем говорит Дейв Абрахамс :
Рекомендация: не копируйте аргументы функции. Вместо этого передайте их по значению и пусть компилятор выполнит копирование.
В коде это означает, что не делайте этого:
void foo(T const& t)
{
auto copy = t;
// ...
}
но сделайте следующее:
void foo(T t)
{
// ...
}
который имеет то преимущество, что вызывающий может использовать foo
следующим образом:
T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue
и выполняется только минимальная работа. Вам понадобится две перегрузки, чтобы сделать то же самое со ссылками, void foo(T const&);
и void foo(T&&);
.
Имея это в виду, я теперь написал мои оцененные конструкторы как таковые:
class T {
U u;
V v;
public:
T(U u, V v)
: u(std::move(u))
, v(std::move(v))
{}
};
В противном случае передача по ссылке const
будет разумной.
Ответ 2
Почти во всех случаях ваша семантика должна быть либо:
bar(foo f); // want to obtain a copy of f
bar(const foo& f); // want to read f
bar(foo& f); // want to modify f
Все другие подписи должны использоваться только экономно и с хорошим обоснованием. Компилятор теперь почти всегда будет работать над этим самым эффективным способом. Вы можете просто написать свой код!
Ответ 3
Передайте параметры по значению, если внутри тела функции нужна копия объекта или нужно только перемещать объект. Передайте const&
, если вам нужен только не мутирующий доступ к объекту.
Пример копирования объекта:
void copy_antipattern(T const& t) { // (Don't do this.)
auto copy = t;
t.some_mutating_function();
}
void copy_pattern(T t) { // (Do this instead.)
t.some_mutating_function();
}
Пример перемещения объекта:
std::vector<T> v;
void move_antipattern(T const& t) {
v.push_back(t);
}
void move_pattern(T t) {
v.push_back(std::move(t));
}
Пример без мутирующего доступа:
void read_pattern(T const& t) {
t.some_const_function();
}
Для обоснования см. эти сообщения в блоге Дэйв Абрахамс и Xiang Fan.
Ответ 4
Подпись функции должна отражать ее предполагаемое использование. Читаемость важна и для оптимизатора.
Это лучшее предварительное условие для оптимизатора для создания самого быстрого кода - по крайней мере в теории, а если не в реальности, то через несколько лет.
Вопросы производительности очень часто переоценивают в контексте передачи параметров. Идеальная пересылка - пример. Такие функции, как emplace_back
, в большинстве случаев очень короткие и встроенные.
Ответ 5
Разумные значения по умолчанию для параметров функции в порядке вероятности использования:
// Read-only or take a copy of f.
void bar(const Foo& f);
// Want to modify f.
void bar(Foo& f);
// Take a copy of f in the most efficient way possible, provide 2 methods:
void bar(const Foo& f); // 1 copy, that possibly reuses memory from the original.
void bar(Foo&& f); // 1 move.
Передача по значению для получения копии менее эффективна, чем приведенная выше рекомендация.
void bar(Foo f) // Take a copy of f.
{
Foo copy = std::move(f);
...
}
// Client passes an lvalue. 1 copy + 1 move.
bar(f); // Bad, because of the extra move, and the quality of the copy
// is worse in some cases. When copying from a const ref
// parameter, a copy may reuse some allocated memory in the
// existing object (say for large strings or vectors). The
// copy from a passed value pays the full cost up front.
// Client passes an rvalue. 2 moves.
bar(std::move(f)); // 2 moves is likely a win over 1 copy, but if you're
// interested in optimizing rvalues then providing an
// rvalue function mentioned in the advice above
// will only have 1 move.
Обратите внимание, что конструкторы классов должны нарушать этот совет и передавать параметры по значению, поскольку он предотвращает комбинаторный взрыв предложений const ref/rvalue при наличии нескольких параметров, и он должен оплатить полную стоимость копии в любом случае, поскольку никакие значения существуют.
Для энтузиаста, который хочет узнать больше, см. рассказ CppCon Herb Sutter 2014: https://www.youtube.com/watch?v=xnqTKD8uD64