Ответ в теме: Ссылки в С++: rvalue- и lvalue- references

#2919

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

template <typename ElementType>
Array<Elementtype> sum(Array<Elementtype> a, Array<Elementtype> b);

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

template <typename ElementType>
Array<Elementtype> sum(Array<Elementtype> &a, Array<Elementtype> &b);

Теперь функция не вызывает конструкторы для своих аргументов при вызове и деструкторы при завершении, т.к. аргументы функции являются лишь альтернативными именами передаваемых объектов. Любое обращение к объекту по ссылке приводит к манипуляции с исходным объектом, однако это не всегда безопасно, например в рассматриваемом примере нам, вероятно, хотелось бы гарантировать, что исходные объекты не изменятся – для этого применяются константные ссылки:

template <typename ElementType>
Array<Elementtype> sum(const Array<Elementtype> &a, const Array<Elementtype> &b);

Теперь если внутри функции будет находиться код, пытающийся изменить значения аргументов мы получим ошибку на этапе компиляции (и это хорошо, т.к. мы узнаем об ошибке сразу).
Кроме того, константные ссылки используются для ссылки на r-значения – так называются все объекты, которые могут располагаться только в правой части операции присваивания (константы и временные объекты). Создать для них обычную (не константную ссылку нельзя):
Array<int> d = sum(a, sum(b, c));
Если аргументами функции sum неконстантные являются ссылки – компилятор выдаст нам ошибку, т.к. sum(b, c) возвращает временный объект. Текст ошибки, который выдаст компилятор gcc для этого примера:

error: invalid initialization of non-const reference of type ‘Array&’ from an rvalue of type ‘Array’
Array c(sum(a, sum(b, c)));

error: in passing argument 2 of ‘Array sum(Array&, Array&) [with ElementType = int]’
Array sum(Array &a, Array &b) {

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

template <class ElementType>
Array(const vector<Elementtype>& vector);

vector<int> a;
Array<int> b;
sum(a, b);

Если возможно неявное преобразование объекта типа vector в объект типа Array, то соответствующий конструктор будет вызван перед выполнением sum, за счет чего будет создан временный объект (rvalue).

Таким образом, неконстантные ссылки могут быть связаны только с l-значениями (описывающими объект, живущий дольше выражения, тем, что может быть расположено в левой части оператора присваивания), а константные – как с r-значениями, так и с l-значениями. В стандарте С++11 были введены специальные ссылки, которые связываются только с r-значениями (rvalue references), задаваемые двумя знаками амперсанда.

cplusplus-references

Из приведенной таблицы видно, что мы можем написать функцию, которая будет принимать константную ссылку, такая функция сможет обрабатывать как rvalue, так и lvalue, однако, в ряде случаев, зная что обрабатывается ссылка на r-значение, можно получить более оптимальный код. Дело в том, что для временного объекта также вызывается конструтор и деструктор:

string reverse(const string& str) {
  string reverse_string(str);
  std::reverse(reverse_string.begin(), reverse_string.end());
  return reverse_string;
}

string_c = reverse(string_a + string_b);

В приведенном примере на вход оператора сложения поступают две строки, внутри оператора создается новый объект, который без сохранения в какой-либо переменной передается функции reverse. Такой объект является временным, но если функция reverse принимает const string& – то она не узнает об этом и создаст внутри новый объект, в который поместит вычисленный результат. Таким образом, будет вызвано два конструктора и один деструктор (для временного объекта).

Каждый конструктор в данном случае связан с динамическим выделением памяти под элементы строки, однако, если бы наша функция знала что на вход подан временный объект – она могла бы использовать его данные без повторного выделения памяти. Именно для этой цели случат rvalue-ссылки:

string reverse(string&& str) {
  string reverse_string(str.buffer);
  str.buffer = nullptr;
  std::reverse(reverse_string.begin(), reverse_string.end());
  return reverse_string;
}

В приведенном примере функция принимает ссылку на временный объект и:

  1. создает новый объект, используя его буфер – экономит на динамическом выделении памяти;
  2. обнуляет указатель на данные временного объекта, чтобы при уничтожении временного объекта не испортить данные объекта reverse_string;
  3. переворачивает строку и возвращает результат

Такой код является правильным, однако обращение к данным объекта (str.buffer) нарушает инкапсуляцию – такой код не является безопасным. В связи с этим, в С++ используются специальные перемещающие конструкторы.