M.2 – Ссылки на r-значения (rvalue-ссылки)
Еще в главе 1 мы упоминали l-значения и r-значения, а затем посоветовали вам не особо о них беспокоиться. Это был справедливый совет до C++11. Но понимание семантики перемещения в C++11 требует повторного изучения этой темы.
l-значения и r-значения
Несмотря на то, что в их названиях есть слово «значение», l-значения и r-значения на самом деле являются не свойствами значений, а скорее свойствами выражений.
Каждое выражение в C++ имеет два свойства: тип (который используется для проверки типа) и категорию значения (которая используется для определенных видов проверки синтаксиса, например, может ли быть выполнено присваивание результату выражения). В C++03 и более ранних версиях l-значения и r-значения были единственными доступными категориями значений.
Фактическое определение того, какие выражения являются l-значениями, а какие – r-значениями, на удивление сложно, поэтому мы рассмотрим его упрощенно, что в значительной степени будет достаточным для наших целей.
Проще всего представить l-значение как функцию или объект (или выражение, вычисляемое в функцию или объект). Всем l-значениям присвоены адреса памяти.
Изначально l-значения были определены как «значения, которые подходят для левой стороны выражения присваивания». Однако позже в язык было добавлено ключевое слово const
, а l-значения были разделены на две подкатегории: изменяемые l-значения, которые можно изменять, и неизменяемые l-значения, которые являются константными.
Об r-значении проще всего думать как о «всем, что не является l-значением». Это, в частности, включает в себя литералы (например, 5), временные значения (например, x + 1
) и анонимные объекты (например, Fraction(5, 2)
). r-значения обычно вычисляются в свои значения, имеют область видимости выражения (они умирают в конце выражения, в котором они находятся), и им не может быть выполнено присваивание. Это правило отсутствия присваивания понятно, потому что присваивание значения применяет побочный эффект к объекту. Поскольку r-значения имеют область видимости выражения, если бы мы должны были присвоить значение r-значению, тогда r-значение либо вышло бы за пределы области видимости до того, как у нас появилась возможность использовать присвоенное значение в следующем выражении (что делает это присваивание бесполезным), или нам пришлось бы использовать переменную с побочным эффектом, применяемую в выражении более одного раза (что к настоящему моменту вы должны знать, вызывает неопределенное поведение!).
Для поддержки семантики перемещения в C++11 были представлены 3 новые категории значений: pr-значения, x-значения и gl-значения. Мы будем в значительной степени игнорировать их, поскольку их понимание не обязательно для эффективных изучения или использования семантики перемещения.
Ссылки на l-значения (lvalue-ссылки)
До C++11 в C++ существовал только один тип ссылки, поэтому он назывался просто «ссылкой». Однако в C++11 его иногда называют ссылкой на l-значение или lvalue-ссылкой. lvalue-ссылки могут быть инициализированы только изменяемыми l-значениями.
lvalue-ссылка | Может быть инициализирована с помощью | Можно изменить |
---|---|---|
Изменяемые l-значения | да | да |
Неизменяемые l-значения | нет | нет |
r-значения | нет | нет |
lvalue-ссылки на константные объекты могут быть инициализированы как l-значениями, так и r-значениями. Однако эти значения нельзя изменить.
lvalue-ссылка на константный объект | Может быть инициализирована с помощью | Можно изменить |
---|---|---|
Изменяемые l-значения | да | нет |
Неизменяемые l-значения | да | нет |
r-значения | да | нет |
lvalue-ссылки на константные объекты особенно полезны, потому что они позволяют нам передавать в функцию любой тип аргумента (l-значение или r-значение) без создания копии аргумента.
Ссылки на r-значения (rvalue-ссылки)
В C++11 был добавлен новый тип ссылки, называемый ссылкой на r-значение (rvalue-ссылкой). rvalue-ссылка – это ссылка, которая предназначена для инициализации (только) r-значением. В то время как lvalue-ссылка создается с использованием одного амперсанда, rvalue-ссылка создается с использованием двойного амперсанда:
int x{ 5 };
int &lref{ x }; // lvalue-ссылка инициализируется l-значением x
int &&rref{ 5 }; // rvalue-ссылка инициализируется r-значением 5
rvalue-ссылки не могут быть инициализированы l-значениями.
rvalue-ссылка | Может быть инициализирована с помощью | Можно изменить |
---|---|---|
Изменяемые l-значения | нет | нет |
Неизменяемые l-значения | нет | нет |
r-значения | да | да |
rvalue-ссылка на константный объект | Может быть инициализирована с помощью | Можно изменить |
---|---|---|
Изменяемые l-значения | нет | нет |
Неизменяемые l-значения | нет | нет |
r-значения | да | нет |
Ссылки на r-значения имеют два полезных свойства. Во-первых, ссылки на r-значения продлевают срок жизни объекта, которым они инициализируются, до срока жизни rvalue-ссылки (lvalue-ссылки на объекты const
тоже могут это делать). Во-вторых, неконстантные rvalue-ссылки позволяют вам изменять r-значение!
Давайте посмотрим на несколько примеров:
#include <iostream>
class Fraction
{
private:
int m_numerator;
int m_denominator;
public:
Fraction(int numerator = 0, int denominator = 1) :
m_numerator{ numerator }, m_denominator{ denominator }
{
}
friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
out << f1.m_numerator << '/' << f1.m_denominator;
return out;
}
};
int main()
{
auto &&rref{ Fraction{ 3, 5 } }; // rvalue-ссылка на временный объект Fraction
// f1 оператора << привязывается к временному объекту, копии не создаются.
std::cout << rref << '\n';
return 0;
} // rref (и временный объект Fraction) здесь выходит из области видимости
Эта программа печатает:
3/5
Как анонимный объект, Fraction(3, 5)
обычно выходит за пределы области видимости в конце выражения, в котором он определен. Однако, поскольку мы инициализируем с его помощью rvalue-ссылку, его продолжительность жизни продлевается до конца блока. Затем мы можем использовать эту rvalue-ссылку для вывода значения дроби Fraction
.
Теперь давайте посмотрим на менее интуитивно понятный пример:
#include <iostream>
int main()
{
// поскольку мы инициализируем rvalue-ссылку литералом,
// здесь создается временный объект со значением 5
int &&rref{ 5 };
rref = 10;
std::cout << rref << '\n';
return 0;
}
Эта программа печатает:
10
Хотя может показаться странным, инициализировать rvalue-ссылку с помощью литерального значения, а затем иметь возможность изменить это значение, но при инициализации r-значения с помощью литерала из этого литерала создается временное значение, поэтому ссылка ссылается на временный объект, а не литеральное значение.
rvalue-ссылки не очень часто используются ни одним из способов, проиллюстрированных выше.
rvalue-ссылки как параметры функции
rvalue-ссылки чаще используются в качестве параметров функции. Это наиболее полезно для перегрузок функций, когда вы хотите иметь разное поведение для аргументов, являющихся l-значением и r-значением.
void fun(const int &lref) // аргументы l-значения выберут эту функцию
{
std::cout << "l-value reference to const\n";
}
void fun(int &&rref) // аргументы r-значения выберут эту функцию
{
std::cout << "r-value reference\n";
}
int main()
{
int x{ 5 };
fun(x); // аргумент с l-значением вызывает версию функции для l-значения
fun(5); // аргумент с r-значением вызывает версию функции для r-значения
return 0;
}
Этот код печатает:
l-value reference to const
r-value reference
Как видите, при передаче l-значения вызов перегруженной функции преобразовался в версию с lvalue-ссылкой. При передаче r-значения вызов перегруженной функции преобразовался в версию с rvalue-ссылкой (она считается лучшим совпадением, чем lvalue-ссылка на const
).
Зачем вообще это нужно? Мы обсудим это более подробно в следующем уроке. Излишне говорить, что это важная часть семантики перемещения.
Одно интересное замечание:
int &&ref{ 5 };
fun(ref);
на самом деле вызывает версию функции с l-значением! Хотя переменная ref
– имеет тип rvalue-ссылка на int
, на самом деле она сама l-значение (как и все именованные переменные). Путаница возникает из-за использования термина r-значение в двух разных контекстах. Подумайте об этом так: именованные объекты – это l-значения. Анонимные объекты – это r-значения. Тип именованного объекта или анонимного объекта не зависит от того, является ли он l-значением или r-значением. Или, другими словами, если бы rvalue-ссылку называли как-нибудь иначе, этой путаницы не было бы.
Возврат rvaue-ссылки
Вы почти никогда не должны возвращать rvalue-ссылку, по той же причине, по которой вы почти никогда не должны возвращать lvalue-ссылку. В большинстве случаев вы вернете висячую ссылку, когда объект, на который указывает ссылка, выходит за пределы области видимости в конце функции.
Небольшой тест
Вопрос 1
Укажите, какая из следующих инструкций с литералами не будет компилироваться:
int main()
{
int x{};
// lvalue-ссылки
int &ref1{ x }; // A
int &ref2{ 5 }; // B
const int &ref3{ x }; // C
const int &ref4{ 5 }; // D
// rvalue-ссылки
int &&ref5{ x }; // E
int &&ref6{ 5 }; // F
const int &&ref7{ x }; // G
const int &&ref8{ 5 }; // H
return 0;
}
Ответ
B, E и G не компилируются.