Проходит ли по ссылке, а затем копирование и передача по значению функционально отличается?
Существует ли функциональное различие между:
void foo(const Bar& bar) {
Bar bar_copy(bar);
// Do stuff with bar_copy
}
и
void foo(Bar bar) {
// Do stuff with bar
}
Ответы
Ответ 1
Да, есть ценная разница.
void foo(Bar bar)
может копировать конструкцию или move-construct bar
, в зависимости от вызывающего контекста.
И когда временное передается foo(Bar bar)
, ваш компилятор может построить это временное место непосредственно там, где ожидается bar
. Кончик шляпы для шаблона мальчика.
Ваша функция void foo(const Bar& bar)
всегда выполняет копию.
Ваша функция void foo(Bar bar)
может выполнять копию или перемещение или, возможно, ни одно из них.
Ответ 2
Да, есть различия. Хотя наиболее очевидным является то, что тип функции изменяется (и, следовательно, тип указателя на ее функцию), есть также некоторые менее очевидные последствия:
Конструкция с возможностью перемещения, но не копируется Bar
Например, предположим следующий вызов foo
:
foo(Bar());
Для первой версии это будет передано ссылкой на const bar
, а затем скопировано с использованием конструктора копирования. Для второй версии компилятор сначала попытается создать конструктор move.
Это означает, что только вторая версия может быть вызвана типами, которые могут быть только конструктивными, например std::unique_ptr
. Фактически, принудительная копия вручную даже не позволит компилировать функцию.
Очевидно, что это можно смягчить, добавив небольшое усложнение:
void foo(Bar&& bar) {
// Do something with bar.
// As it is an rvalue-reference, you need not copy it.
}
void foo(Bar const& bar) {
Bar bar_copy(bar);
foo(std::move(bar_copy));
}
Спецификаторы доступа
Интересно, что есть еще одно отличие: контекст, в котором проверяются разрешения доступа.
Рассмотрим следующий Bar
:
class Bar
{
Bar(Bar const&) = default;
Bar(Bar&&) = default;
public:
Bar() = default;
friend int main();
};
Теперь версия reference-and-copy выйдет из строя, а версия с параметром-значением не будет жаловаться:
void fooA(const Bar& bar)
{
//Bar bar_copy(bar); // error: 'constexpr Bar::Bar(const Bar&)' is private
}
void fooB(Bar bar) { } // OK
Так как мы объявили main
своим другом, следующий вызов разрешен (обратите внимание, что друг не понадобится, если, например, фактический вызов был выполнен в функции static
члена Bar
):
int main()
{
fooB(Bar()); // OK: Main is friend
}
Полнота Bar
на сайте вызова
Как уже отмечалось в комментариях, если вы хотите, чтобы Bar
был неполным типом на сайте вызова, можно использовать версию сквозной ссылки, так как это не требует, чтобы сайт вызова был способный выделить объект типа Bar
.
Копирование побочных эффектов Elision
С++ 11 12.8/31:
При выполнении определенных критериев реализация допускает опустить конструкцию копирования/перемещения класса объект, даже если конструктор copy/move и/или деструктор объекта имеют побочные эффекты. В таких случаях, реализация рассматривает источник и цель пропущенной операции копирования/перемещения как просто две разные способы обращения к одному и тому же объекту [...]
- [...]
- когда временный объект класса, который не был привязан к ссылке (12.2), будет скопирован/перемещен к объекту класса с тем же cv-неквалифицированным типом операция копирования/перемещения может быть опущена построение временного объекта непосредственно в цель пропущенной копии/перемещения
- [...]
Очевидно, что только эта версия по умолчанию соответствует этому критерию - после прохождения по ссылке параметр привязан к ссылке в конце концов. Помимо наблюдаемой разницы, это также означает, что оптимизационная oppurtunity теряется.
Ответ 3
Есть некоторые отличия.
void foo(const Bar& bar) {
Bar bar_copy(bar);
// Do stuff with bar_copy
}
не позволяет избежать копирования, даже если bar
был временным и избегать также перемещения bar
.