Лучше ли в C ++ передавать по значению или передавать по константе?


213

Лучше ли в C ++ передавать по значению или передавать по константе?

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


Ответы:


203

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

Это было особенно верно до существования семантики перемещения . Причина проста: если вы передали по значению, нужно было сделать копию объекта, и, за исключением очень маленьких объектов, это всегда дороже, чем передача ссылки.

С C ++ 11 мы получили семантику перемещения . Вкратце, семантика перемещения позволяет в некоторых случаях передавать объект «по значению», не копируя его. В частности, это тот случай , когда объект , который вы передаете является Rvalue .

Само по себе перемещение объекта по крайней мере так же дорого, как передача по ссылке. Однако во многих случаях функция все равно будет внутренне копировать объект - т.е. она получит владение аргументом. 2

В этих ситуациях мы имеем следующий (упрощенный) компромисс:

  1. Мы можем передать объект по ссылке, а затем выполнить внутреннее копирование.
  2. Мы можем передать объект по значению.

«Передача по значению» по-прежнему вызывает копирование объекта, если только объект не является значением. В случае значения r объект можно вместо этого переместить, так что второй случай внезапно перестает быть «копировать, затем перемещать», а «перемещать, а затем (потенциально) перемещать снова».

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


Историческая справка:

Фактически, любой современный компилятор должен быть в состоянии выяснить, стоит ли передавать по значению дорого, и неявно преобразовывать вызов, чтобы использовать const ref, если это возможно.

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

Но в целом компилятор не может определить это, и появление семантики перемещения в C ++ сделало эту оптимизацию гораздо менее актуальной.


1 Например, Скотт Мейерс, Эффективный C ++ .

2 Это особенно часто верно для конструкторов объектов, которые могут принимать аргументы и сохранять их внутри, чтобы быть частью состояния построенного объекта.


хммм ... я не уверен, что стоит пройти мимо реф. double-s
sergtk

3
Как обычно, Boost помогает здесь. boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm содержит шаблонные материалы для автоматического определения, когда тип является встроенным типом (полезно для шаблонов, где вы иногда не можете узнать это легко).
CesarB

13
Этот ответ упускает важный момент. Чтобы избежать нарезки, вы должны пройти по ссылке (const или иначе). См stackoverflow.com/questions/274626/...
ChrisN

6
@ Крис: верно. Я пропустил всю часть полиморфизма, потому что это совершенно другая семантика. Я считаю, что OP (семантически) означает передачу аргумента «по значению». Когда требуется другая семантика, вопрос даже не ставится сам по себе.
Конрад Рудольф

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, примите ее по значению.

«Примитив» выше означает в основном небольшие типы данных, которые имеют длину несколько байтов и не являются полиморфными (итераторы, объекты функций и т. Д.) Или дорогостоящими для копирования. В этой статье есть еще одно правило. Идея состоит в том, что иногда кто-то хочет сделать копию (в случае, если аргумент не может быть изменен), а иногда он не хочет (в случае, если кто-то хочет использовать сам аргумент в функции, если в любом случае аргумент был временным , например). В документе подробно объясняется, как это можно сделать. В 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 */
}

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

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

Я нашел ваши правила хорошими, но я не уверен насчет первой части, в которой вы говорите о том, что не проходите мимо, так как ссылка ускорит его. да, конечно, но не пропускать что-либо в качестве ссылки просто оптимизация не имеет смысла вообще. если вы хотите изменить передаваемый в стек объект, сделайте это с помощью ссылки. если нет, передайте его по значению. если вы не хотите его менять, передайте его как const-ref. оптимизация, которая идет с передачей по значению, не должна иметь значения, так как вы получаете другие вещи при передаче в качестве ссылки. я не понимаю "хочу скорость?" если ты собираешься выполнить эти операции, ты все равно
передашь

Йоханнес: Мне понравилась эта статья, когда я ее прочитал, но я был разочарован, когда попробовал. Этот код не удалось на GCC и MSVC. Я что-то пропустил или это не работает на практике?
user541686

Я не думаю, что согласен с тем, что если вы все равно хотите сделать копию, вы передадите ее по значению (вместо const ref), а затем переместите ее. Посмотрите на это так, что более эффективно: копия и ход (у вас может быть даже 2 копии, если вы передадите ее вперед), или просто копия? Да, есть некоторые особые случаи с обеих сторон, но если ваши данные не могут быть перемещены в любом случае (например, POD с тоннами целых чисел), нет необходимости в дополнительных копиях.
Ион Тодирел

2
Mehrdad, не уверен, что вы ожидали, но код работает, как ожидалось
Ион Тодирел

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

12

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


Для не родных типов вы можете (в зависимости от того, насколько хорошо компилятор оптимизирует код) повысить производительность, используя ссылки на константы, а не просто ссылки.
О.Дж.

9

Как уже было отмечено, это зависит от типа. Для встроенных типов данных лучше всего передавать по значению. Даже некоторые очень маленькие структуры, такие как пара целых, могут работать лучше, передавая значение.

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

Если вы занимаетесь программированием шаблонов, вы обычно вынуждены всегда проходить через const ref, так как не знаете, какие типы передаются. Передача штрафов за передачу чего-то плохого по значению намного хуже, чем штрафы за передачу встроенного типа. по конст. исх.


Примечание по терминологии: структура, содержащая миллион целых, все еще является «типом POD». Возможно, вы имеете в виду «для встроенных типов лучше передавать по значению».
Стив Джессоп

6

Это то, чем я обычно работаю при разработке интерфейса не шаблонной функции:

  1. Передайте по значению, если функция не хочет изменять параметр, а значение дешево копировать (int, double, float, char, bool и т. Д. ... Обратите внимание, что std :: string, std :: vector и остальные из контейнеров в стандартной библиотеке нет)

  2. Передача по указателю const, если значение дорого копировать, и функция не хочет изменять указанное значение, а NULL - это значение, которое обрабатывает функция.

  3. Передайте неконстантный указатель, если значение дорого копировать, и функция хочет изменить указанное значение, а NULL - это значение, которое обрабатывает функция.

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

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


Добавьте std::optionalк картинке и вам больше не нужны указатели.
Фиолетовый Жираф

5

Похоже, вы получили свой ответ. Передача по значению стоит дорого, но дает вам копию для работы, если вам это нужно.


Я не уверен, почему это было отклонено? Это имеет смысл для меня. Если вам нужно сохранить текущее значение, передайте его по значению. Если нет, передайте ссылку.
Тотти

4
Это полностью зависит от типа. Использование типа POD (простые старые данные) по ссылке может фактически снизить производительность, вызывая больше обращений к памяти.
Торлак

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

4
Это не так очевидно, я видел много кода людьми, которые не совсем понимают, как работают компьютеры, передавая простые вещи по const ref, потому что им сказали, что это лучшее, что можно сделать.
Торлак

4

Как правило, лучше передавать по константной ссылке. Но если вам нужно изменить аргумент вашей функции локально, вам лучше использовать передачу по значению. Для некоторых базовых типов производительность в целом одинакова как для передачи по значению, так и по ссылке. Фактически ссылка, представленная указателем, поэтому вы можете ожидать, например, что для указателя обе передачи одинаковы с точки зрения производительности, или даже передача по значению может быть быстрее из-за ненужной разыменования.


Если вам нужно изменить копию параметра вызываемого, вы можете сделать копию в вызываемом коде, а не передавать по значению. IMO, вы, как правило, не должны выбирать API, основываясь на таких деталях реализации: источник вызывающего кода в любом случае одинаков, но его объектный код - нет.
Стив Джессоп

Если вы передадите значение, копия будет создана. И IMO не имеет значения, каким образом вы создаете копию: с помощью аргумента, передаваемого по значению или локально - это то, что касается C ++. Но с точки зрения дизайна я с тобой согласен. Но я описываю здесь только функции C ++ и не касаюсь дизайна.
sergtk

1

Как правило, значение для не-классовых типов и константная ссылка для классов. Если класс действительно маленький, то, вероятно, лучше передать по значению, но разница минимальна. Чего вы действительно хотите избежать, так это передавая некоторый гигантский класс по значению и дублируя его - это будет иметь огромное значение, если вы передадите, скажем, std :: vector с довольно большим количеством элементов в нем.


Я понимаю, что на std::vectorсамом деле размещает свои элементы в куче, а сам векторный объект никогда не растет. Ой, подожди. Однако, если операция вызывает создание копии вектора, она на самом деле будет дублировать все элементы. Это было бы плохо.
Стивен Лу

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

1

Передайте по значению для небольших типов.

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

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

Теперь вызывающий код будет делать:

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

И только один объект будет создан и перемещен непосредственно в член name_класса Person. Если вы передадите константную ссылку, для ее размещения потребуется сделать копию 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.