Является ли pass-by-value разумным дефолтом в С++ 11? - программирование

Является ли pass-by-value разумным дефолтом в С++ 11?

В традиционном С++ переход по значениям в функции и методы замедляется для больших объектов и, как правило, неодобрен. Вместо этого программисты на C++ склонны передавать ссылки, что происходит быстрее, но в них вводятся всевозможные сложные вопросы, связанные с владением, и особенно в области управления памятью (в том случае, если объект выделен в виде кучи)

Теперь, в С++ 11, мы имеем ссылки Rvalue и перемещаем конструкторы, а это означает, что можно реализовать большой объект (например, std::vector), который дешево передавать по значению в и из функции.

Итак, означает ли это, что значение по умолчанию должно быть передано по значению для экземпляров типов, таких как std::vector и std::string? Как насчет пользовательских объектов? Какая новая лучшая практика?

4b9b3361

Ответ 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