Чи краще в C ++ проходити за значенням або проходити за постійною посиланням?


213

Чи краще в C ++ проходити за значенням або проходити за постійною посиланням?

Мені цікаво, яка краща практика. Я розумію, що постійне посилання має забезпечувати кращі показники роботи програми, оскільки ви не створюєте копію змінної.


Відповіді:


203

Раніше зазвичай рекомендуються найкраща практика 1 для використання проходу по константностей іому для всіх типів , для вбудованих типів (крім char, int, doubleі т.д.), для ітераторів і функціональних об'єкти (лямбда, класів , що випливають з std::*_function).

Особливо це стосувалося до існування рухової семантики . Причина проста: якщо ви перейшли за значенням, довелося зробити копію об'єкта і, за винятком дуже маленьких об'єктів, це завжди дорожче, ніж проходження посилання.

За допомогою C ++ 11 ми отримали семантику руху . У двох словах, семантика переміщення дозволяє в деяких випадках об'єкт можна передавати «за значенням», не копіюючи його. Зокрема, це той випадок, коли об'єкт, який ви передаєте, є релевантним .

Само по собі переміщення предмета все ще принаймні так само дорого, як і проходження посиланням. Однак у багатьох випадках функція все-таки внутрішньо копіює об’єкт - тобто вона буде мати право власності на аргумент. 2

У цих ситуаціях ми маємо наступні (спрощені) компроміси:

  1. Ми можемо передати об’єкт за посиланням, а потім скопіювати внутрішньо.
  2. Ми можемо передати об’єкт за значенням.

"Передати значення" все ще призводить до того, що об'єкт буде скопійовано, якщо тільки об'єкт не є оцінкою. У випадку ревальвації об'єкт може бути переміщений замість цього, так що другий випадок раптом більше не "скопіювати, потім перемістити", а "перемістити, потім (потенційно) перемістити знову".

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


Історична записка:

Насправді, будь-який сучасний компілятор повинен мати можливість з'ясувати, коли передача за значенням дорога, і неявно перетворити виклик, щоб використовувати const ref, якщо це можливо.

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

Але в цілому компілятор не може цього визначити, і поява семантики переміщення в C ++ зробила цю оптимізацію набагато менш актуальною.


1 Напр. У Скотта Майєрса, Ефективний C ++ .

2 Особливо часто це стосується конструкторів об'єктів, які можуть брати аргументи та зберігати їх всередині, щоб бути частиною стану побудованого об'єкта.


хммм ... я не впевнений, що варто пройти повз посилання. double-s
sergtk

3
Як завжди, тут допомагає підвищення. boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm має шаблон шаблону, який автоматично визначає, коли тип є вбудованим типом (корисно для шаблонів, де ви іноді не можете цього легко знати).
CesarB

13
Ця відповідь пропускає важливий момент. Щоб уникнути нарізки, потрібно пройти посилання (const чи іншим способом). Див stackoverflow.com/questions/274626 / ...
ChrisN

6
@Chris: правильно. Я залишив всю частину поліморфізму, тому що це зовсім інша семантика. Я вважаю, що ОП (семантично) означало проходження аргументу "за значенням" Коли потрібна інша семантика, питання навіть не виникає.
Конрад Рудольф

98

Редагувати: Нова стаття Дейва Абрахамса на cpp-next:

Хочете швидкості? Перейти за значенням.


Передача за значенням для структур, де копіювання дешеве, має додаткову перевагу в тому, що компілятор може вважати, що об'єкти не псевдоніми (не є однаковими об'єктами). Використовуючи пропускний посилання, компілятор не може вважати це завжди. Простий приклад:

foo * f;

void bar(foo g) {
    g.i = 10;
    f->i = 2;
    g.i += 5;
}

компілятор може оптимізувати його в

g.i = 15;
f->i = 2;

оскільки він знає, що f і g не мають однакового місця. якби g був посиланням (foo &), компілятор не міг би цього припустити. оскільки gi тоді може бути псевдонімом f-> i і має мати значення 7. тож компілятору доведеться повторно отримати нове значення gi з пам'яті.

Для більш дотичних правил, ось хороший набір правил, знайдений у статті Move Constructors (настійно рекомендується прочитати).

  • Якщо функція має намір змінити аргумент як побічний ефект, прийміть його за допомогою посилань, які не мають const.
  • Якщо функція не змінює свій аргумент і аргумент примітивного типу, візьміть його за значенням.
  • В іншому випадку прийміть це за посиланням const, за винятком наступних випадків
    • Якщо функція все-таки повинна зробити копію посилання на const, будь-ласка, візьміть її за значенням.

Вище "примітивні" означають, в основному, невеликі типи даних, які мають довжину в декілька байтів і не є поліморфними (ітератори, об'єкти функцій тощо) або копіюють дорого. У цьому документі є ще одне правило. Ідея полягає в тому, що іноді хочеться зробити копію (у випадку, якщо аргумент неможливо змінити), а іноді не хоче (у випадку, якщо хтось хоче використовувати сам аргумент у функції, якщо аргумент все одно був тимчасовим , наприклад). У статті детально пояснено, як це можна зробити. У мові C ++ 1x ця техніка може бути використана в основному за допомогою мовної підтримки. До цього часу я б пішов з вищезазначеними правилами.

Приклади: щоб зробити верхній регістр рядка та повернути верхні регістри, завжди слід проходити за значенням: все одно потрібно взяти його копію (не можна змінити посилання const безпосередньо) - тому краще зробіть її максимально прозорою для абонент і зробить цю копію рано, щоб абонент міг максимально оптимізувати - як детальніше в цьому документі:

my::string uppercase(my::string s) { /* change s and return it */ }

Однак якщо параметр все одно не потрібно змінювати, візьміть його за посиланням на const:

bool all_uppercase(my::string const& s) { 
    /* check to see whether any character is uppercase */
}

Однак, якщо ви маєте на меті цього параметра написати щось в аргумент, то передайте це за допомогою посилань, які не мають const

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}

Я знайшов ваші правила хорошими, але я не впевнений, що про першу частину, де ви говорите про не передачу його, як посилання, це пришвидшить його. Так, звичайно, але не передавати щось як перегляд просто оптимізація взагалі не має сенсу. якщо ви хочете змінити об'єкт стека, який ви передаєте, зробіть це за посиланням. якщо ви цього не зробите, передайте його за значенням. якщо ви не хочете його змінити, передайте це як const-ref. оптимізація, яка поставляється з прохідною вартістю, не повинна мати значення, оскільки ви отримуєте інші речі, переходячи як посилання я не розумію "хочу швидкість?" Січе, якщо ти де будеш виконувати ці оп, то все одно перейдеш за цінністю ..
chikuba

Йоганнес: Я любив цю статтю, коли читав її, але розчарувався, коли спробував її. Цей код не вдався як для GCC, так і MSVC. Я щось пропустив, чи це не працює на практиці?
користувач541686

Я не думаю, що я згоден, що якщо ви хочете все-таки зробити копію, ви передасте її за значенням (замість const ref), а потім перемістіть її. Подивіться на це так, що ефективніше - копія та хід (у вас навіть 2 копії, якщо ви передаєте її вперед), або просто копія? Так, є окремі випадки в обидві сторони, але якщо ваші дані все-таки не можна переміщувати (наприклад, ПОД з тоннами цілих чисел), не потрібно зайвих копій.
Іон Тодірел

2
Мехрдад, не впевнений, що ти очікував, але код працює так, як очікувалося
Іон Тодірел

Я вважаю за необхідне копіювання лише для того, щоб переконати компілятора, що типи не перекривають дефіцит мови. Я вважаю за краще використовувати GCC __restrict__(які також можуть працювати над посиланнями), ніж робити зайві копії. Занадто поганий стандарт C ++ не прийняв restrictключове слово C99 .
Руслан

12

Залежить від типу. Ви додаєте невеликі накладні витрати на необхідність зробити довідку та переналагодження. Для типів розміром, рівним або меншим, ніж покажчики, які використовують за замовчуванням копіюючий копій, можливо, швидше пройти за значенням.


Для неродніх типів ви можете (залежно від того, наскільки добре компілятор оптимізує код), отримати підвищення продуктивності, використовуючи посилання const замість просто посилань.
ОВ.

9

Як було зазначено, це залежить від типу. Для вбудованих типів даних найкраще передавати значення. Навіть деякі дуже маленькі структури, такі як пара вкладених елементів, можуть працювати краще, передаючи значення.

Ось приклад, припустимо, що у вас є ціле значення, і ви хочете передати його в іншу процедуру. Якщо це значення було оптимізоване для зберігання в реєстрі, то, якщо ви хочете передати це посилання, воно спочатку має бути збережене в пам'яті, а потім вказівник на цю пам'ять, розміщений у стеку, щоб здійснити виклик. Якщо воно передається за значенням, все, що потрібно, - це регістр, висунутий на стек. (Деталі трохи складніші, ніж ті, що даються для різних систем виклику та процесорів).

Якщо ви займаєтеся програмуванням шаблонів, зазвичай ви змушені завжди проходити через const ref, оскільки ви не знаєте типів, які передаються. Штрафи за передачу чогось поганого за значенням набагато гірші, ніж штрафи за передачу вбудованого типу за const ref.


Примітка щодо термінології: структура, що містить мільйон точок, все ще є "POD". Можливо, ви маєте на увазі "для вбудованих типів найкраще передати значення".
Стів Джессоп

6

Це те, над чим я зазвичай працюю, розробляючи інтерфейс нешаблонної функції:

  1. Передайте значення, якщо функція не хоче змінювати параметр, а значення недорого копіювати (int, double, float, char, bool тощо). Зауважте, що std :: string, std :: vector та інше контейнерів у стандартній бібліотеці НЕ)

  2. Пройдіть по const pointer, якщо значення дорого копіюється, а функція не хоче змінювати вказане значення, а NULL - це значення, яке функціонує функція.

  3. Пройдіть повз покажчик non-const, якщо значення дорого копіюється, а функція хоче змінити вказане значення, а NULL - це значення, яке функціонує функція.

  4. Передайте посилання const, коли значення дорого копіюється, і функція не хоче змінювати згадане значення, і NULL не було б дійсним значенням, якби замість цього використовувався покажчик.

  5. Пройдіть по посиланням без переліку, коли значення копіюється дорого, і функція хоче змінити вказане значення, і NULL не було б дійсним значенням, якби замість цього використовувався покажчик.


Додайте std::optionalдо зображення, і вам більше не потрібні покажчики.
Фіолетова жирафа

5

Здається, ти отримав свою відповідь. Передача за вартістю дорога, але дає вам копію, з якою можна працювати, якщо вона вам потрібна.


Я не впевнений, чому це було проголосовано? Для мене це має сенс. Якщо вам знадобиться значення, яке зберігається в даний час, тоді передайте значення. Якщо ні, передайте посилання.
Totty

4
Це повністю залежить від типу. Здійснення типу POD (звичайні старі дані) за посиланням може насправді знизити продуктивність, викликаючи більше доступу до пам'яті.
Торлак

1
Очевидно, що передача int за посиланням нічого не економить! Я думаю, що питання передбачає речі, які є більшими за покажчик.
GeekyMonkey

4
Це не так очевидно, я бачив багато коду людей, які не справді розуміють, як комп'ютери працюють, передаючи прості речі за допомогою const ref, тому що їм сказали, що це найкраще робити.
Торлак

4

Як правило, краще проходити посилання const. Але якщо вам потрібно змінити функціональний аргумент на локальному рівні, вам слід скористатися передачею за значенням. Для деяких основних типів продуктивність в цілому однакова як для проходження за значенням, так і за посиланням. Насправді посилання, представлене внутрішньо покажчиком, саме тому, можна очікувати, що, наприклад, і для обох покажчиків однакові за показниками продуктивності, або навіть передача за значенням може бути швидшою через непотрібну відсічку.


Якщо вам потрібно змінити копію цього параметра, ви можете зробити копію коду, що викликається, а не передавати його за значенням. ІМО, як правило, не слід вибирати API на основі детальної інформації про реалізацію, подібну до цього: джерело викликового коду є таким самим способом, але його об'єктний код не є.
Стів Джессоп

Якщо ви переходите за значенням, створюється копія. І IMO не має значення, яким способом ви створюєте копію: через аргумент, що передається за значенням або локально - це стосується C ++. Але з точки зору дизайну я з вами згоден. Але я описую тут лише функції C ++ і не торкаюся дизайну.
sergtk

1

Як правило, величина для некласових типів та посилання const для класів. Якщо клас дійсно малий, то, ймовірно, краще передавати значення, але різниця мінімальна. Чого ви насправді хочете уникнути, це передавати якийсь гігантський клас за значенням і все це дублювати - це призведе до величезної різниці, якщо ви передасте, скажімо, std :: vector з досить великою кількістю елементів у ньому.


Я розумію, що std::vectorнасправді розподіляє свої елементи на купу, а сам векторний об'єкт ніколи не росте. Чекай. Якщо операція призведе до того, що буде зроблена копія вектора, вона фактично піде і дублює всі елементи. Це було б погано.
Стівен Лу

1
Так, це я думав. sizeof(std::vector<int>)є постійним, але передавання його за значенням все одно буде копіювати вміст за відсутності будь-якої чіткості компілятора.
Пітер

1

Передайте значення за малі типи.

Передайте посилання const для великих типів (визначення великого може змінюватись в залежності від машин), але в C ++ 11 передайте значення, якщо ви збираєтеся споживати дані, оскільки ви можете використовувати семантику переміщення. Наприклад:

class Person {
 public:
  Person(std::string name) : name_(std::move(name)) {}
 private:
  std::string name_;
};

Тепер код виклику зробив би:

Person p(std::string("Albert"));

І тільки один об’єкт буде створений і переміщений безпосередньо в член name_в класі Person. Якщо ви проходите через посилання const, для його введення потрібно буде зробити копію name_.


-5

Проста різниця: - У функції у нас є параметр вводу та виводу, тому якщо параметр введення та виходу є однаковим, тоді використовуйте виклик за посиланням інше, якщо параметр вводу та виходу відрізняються, тоді краще використовувати виклик за значенням.

приклад void amount(int account , int deposit , int total )

вхідний параметр: рахунок, параметр виходу депозиту: загальний

вхід і вихід - це різні дзвінки з використанням vaule

  1. void amount(int total , int deposit )

вхідний загальний обсяг виходу депозиту

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