Является ли передача по значению разумным значением по умолчанию в C ++ 11?

142

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

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

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

Дерек Турн
источник
22
pass by reference ... which introduces all sorts of complicated questions around ownership and especially around memory management (in the event that the object is heap-allocated), Я не понимаю, как это сложно или проблематично для владельца? Может быть, я что-то пропустил?
Iammilind
1
@iammilind: пример из личного опыта. Один поток имеет строковый объект. Он передается функции, которая порождает другой поток, но неизвестный вызывающей стороне, функция восприняла строку как, const std::string&а не как копию. Затем вышел первый поток ...
Zan Lynx
12
@ZanLynx: Это звучит как функция, которая явно никогда не предназначалась для вызова в качестве функции потока.
Николь Болас
5
Соглашаясь с iammilind, я не вижу никаких проблем. Передача по константной ссылке должна быть по умолчанию для "больших" объектов и по значению для более мелких объектов. Я бы поставил предел между большим и малым в 16 байтов (или 4 указателя в 32-битной системе).
JN
3
Херб Саттер возвращается к основам! Основы презентации Modern C ++ на CppCon были подробно рассмотрены. Видео здесь .
Крис Дрю

Ответы:

138

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

Рекомендация: не копируйте аргументы вашей функции. Вместо этого передайте их по значению и позвольте компилятору выполнить копирование.

В коде это означает, что не делайте этого:

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все еще является разумной.

Люк Дантон
источник
29
+1, особенно для последнего бита :) Не следует забывать, что конструкторы Move можно вызывать только в том случае, если не предполагается, что объект для перемещения не изменится после этого: например SomeProperty p; for (auto x: vec) { x.foo(p); }, не подходит. Кроме того, конструкторы перемещения имеют стоимость (чем больше объект, тем выше стоимость), хотя const&по существу бесплатны.
Матье М.
25
@MatthieuM. Но важно знать, что «чем больше объект, тем выше стоимость» на самом деле означает: «больше» на самом деле означает «чем больше у него переменных-членов». Например, перемещение std::vectorс миллионом элементов стоит столько же, сколько перемещение с пятью элементами, поскольку перемещается только указатель на массив в куче, а не каждый объект в векторе. Так что это на самом деле не такая уж большая проблема.
Лукас
+1 Я также склонен использовать конструкцию pass-by-value-then-move, так как я начал использовать C ++ 11. Это заставляет меня чувствовать себя немного неловко, так как мой код теперь std::moveповсюду ..
stijn
1
Есть один риск const&, который несколько раз сбил меня с толку. void foo(const T&); int main() { S s; foo(s); }, Это может скомпилировать, даже если типы разные, если есть конструктор T, который принимает S в качестве аргумента. Это может быть медленным, потому что большой T-объект может быть создан. Вы можете подумать, что передали ссылку, не копируя, но, возможно, так оно и есть. Смотрите этот ответ на вопрос, который я задал для более. В основном, &обычно привязывается только к lvalues, но есть исключение для rvalue. Есть альтернативы.
Аарон МакДейд
1
@AaronMcDaid Это старые новости, в том смысле, что вы всегда должны были о них знать еще до C ++ 11. И ничего особенного не изменилось в этом отношении.
Люк Дантон
71

Почти во всех случаях ваша семантика должна быть либо:

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

Все остальные подписи должны использоваться только экономно и с достаточным основанием. Компилятор теперь всегда будет работать наиболее эффективным способом. Вы можете просто продолжить писать свой код!

Ayjay
источник
2
Хотя я предпочитаю передавать указатель, если я собираюсь изменить аргумент. Я согласен с руководством по стилю Google, которое делает его более очевидным, что аргумент будет изменен без необходимости повторной проверки подписи функции ( google-styleguide.googlecode.com/svn/trunk/… ).
Макс Либберт
40
Причина, по которой мне не нравятся проходящие указатели, заключается в том, что это добавляет возможное состояние отказа моим функциям. Я пытаюсь написать все свои функции так, чтобы они были достоверно правильными, так как это значительно уменьшает пространство, в котором могут скрываться ошибки. foo(bar& x) { x.a = 3; }Это чертовски намного надежнее (и читабельнее!), Чемfoo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;
Ayjay
22
@Max Lybbert: с параметром указателя вам не нужно проверять сигнатуру функции, но вам нужно проверить документацию, чтобы узнать, разрешено ли передавать нулевые указатели, если функция примет владение и т.д. ИМХО, параметр указателя передает гораздо меньше информации, чем неконстантная ссылка. Однако я согласен, что было бы неплохо иметь визуальную подсказку на сайте вызовов, что аргумент может быть изменен (например, refключевое слово в C #).
Люк Турэй
Что касается передачи по значению и использования семантики перемещения, я считаю, что эти три варианта лучше объясняют предполагаемое использование параметра. Это рекомендации, которым я всегда следую.
Тревор Хикки
1
@AaronMcDaid is shared_ptr intended to never be null? Much as (I think) unique_ptr is?Оба эти предположения неверны.unique_ptrи shared_ptrможет содержать нуль / nullptrзначения. Если вы не хотите беспокоиться о нулевых значениях, вы должны использовать ссылки, потому что они никогда не могут быть нулевыми. Вам также не придется печатать ->, что вас раздражает :)
Julian
10

Передайте параметры по значению, если внутри тела функции вам нужна копия объекта или вам нужно только переместить объект. Пройти мимо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();
}

Для обоснования, см. Эти сообщения в блоге Дэйва Абрахамса и Сян Фана .

Эдвард Брей
источник
0

Подпись функции должна отражать ее предполагаемое использование. Читаемость важна и для оптимизатора.

Это лучшее предварительное условие для оптимизатора для создания самого быстрого кода - по крайней мере в теории, а если не в реальности, то через несколько лет.

Вопросы производительности очень часто переоцениваются в контексте передачи параметров. Идеальная пересылка - пример. Такие функции, как emplace_backправило, в основном очень короткие и встроенные.

Патрик Фромберг
источник