Чи розумне значення за промовчанням у програмі C ++ 11?


142

У традиційному C ++ передача за значенням функцій та методів для великих об'єктів повільна і, як правило, нахмурена. Натомість програмісти на C ++, як правило, передають посилання навколо, що швидше, але це вводить усілякі складні питання щодо власності та особливо щодо управління пам'яттю (у випадку, якщо об’єкт виділено купою)

Тепер, у C ++ 11, у нас є посилання Rvalue та конструктори переміщення, що означає, що можливо реалізувати великий об'єкт (на зразок an 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, я не бачу жодної проблеми. Передача по const посиланням має бути за замовчуванням для "великих" об'єктів, а також значенням для менших об'єктів. Я поставив би обмеження між великим і малим приблизно в 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 Constructors можна викликати лише в тому випадку, якщо об'єкт, з якого рухатися, не очікується, що він буде незмінним після цього: наприклад SomeProperty p; for (auto x: vec) { x.foo(p); }, не підходить, наприклад. Крім того, конструктори Move мають вартість (чим більший об'єкт, тим вище вартість), хоча const&вони по суті безкоштовні.
Матьє М.

25
@MatthieuM. Але важливо знати, що "чим більший об'єкт, тим вище вартість" ходу насправді означає: "більший" насправді означає "чим більше змінних членів у нього". Наприклад, переміщення std::vectorодного мільйона елементів коштує так само, як переміщення одного з п'ятьма елементами, оскільки переміщується лише вказівник на масив на купі, а не кожен об’єкт у векторі. Тож насправді це не велике питання.
Лукас

+1 Я також схильний використовувати конструкцію "перейти до значення", а потім перемістити, оскільки я почав використовувати C ++ 11. Це змушує мене відчувати себе трохи неприємно, оскільки мій код тепер є std::move
повсюдно

1
Є один ризик const&, який мене кілька разів відключив. void foo(const T&); int main() { S s; foo(s); }. Це може компілюватися, навіть якщо типи різні, якщо є конструктор T, який бере аргумент S. Це може бути повільним, оскільки великий Т-об’єкт може бути побудований. Ви можете подумати, що ви передаєте посилання без копіювання, але ви можете. Дивіться цю відповідь на запитання, яке я просив більше. Як &правило , зазвичай пов'язується лише з значеннями, але є виняток для 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

Усі інші підписи повинні використовуватися лише скупо та з хорошим обґрунтуванням. Зараз компілятор майже завжди опрацьовує це найефективнішим чином. Ви можете просто продовжити написання коду!


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;
Айяй,

22
@Max Lybbert: За допомогою параметра вказівника вам не потрібно перевіряти підпис функції, але вам потрібно перевірити документацію, щоб знати, чи вам дозволяється передавати нульові покажчики, чи функція прийме власність тощо. IMHO, параметр вказівника передає набагато менше інформації, ніж посилання, що не стосується const. Однак я погоджуюся, що було б непогано мати візуальну підказку на сайті виклику, що аргумент може бути змінено (наприклад, refключове слово в C #).
Люк Турей

Що стосується проходження за значенням і покладаючись на семантику переміщення, я вважаю, що ці три варіанти краще підходять до пояснення наміченого використання параметра. Таких рекомендацій я також завжди дотримуюся.
Тревор Хікі

1
@AaronMcDaid is shared_ptr intended to never be null? Much as (I think) unique_ptr is?Обидва ці припущення є невірними. unique_ptrі shared_ptrможе містити null / 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правило, дуже короткі та в будь-якому випадку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.