Проходит ли по ссылке, а затем копирование и передача по значению функционально отличается?

Существует ли функциональное различие между:

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.