Мы уже ранее рассматривали l-values и r-values. Тогда мы говорили, что вам не нужно слишком беспокоиться о них. И это было правдой до C++11. Сейчас же, для понимания семантики перемещения, нам нужно пересмотреть эту тему.
l-values и r-values
Несмотря на то, что в обоих терминах есть слово «value» (значение), l-values и r-values на самом деле являются не свойствами значений, а скорее свойствами выражений.
Каждое выражение в языке C++ имеет два свойства: тип и категорию значения (определяет, можно ли результат выражения присвоить другому объекту). В C++03 и в более ранних версиях С++ l-values и r-values были единственными категориями значений.
О l-value проще всего думать, как о функции, объекте или переменной (или выражении, результатом которого является функция, объект или переменная), которая имеет свой адрес памяти. Изначально l-values были определены как «значения, которые должны находиться в левой части операции присваивания». Однако позже в язык С++ было добавлено ключевое слово const, и l-values были разделены на две подкатегории:
Модифицируемые l-values, которые можно изменить (например, переменной x
можно присвоить другое значение).
Немодифицируемые l-values, которые являются const (например, константа PI
).
О r-value проще всего думать, как «обо всем остальном, что не является l-value». Это литералы (например, 5
), временные значения (например, x + 1
) и анонимные объекты (например, Fraction(7, 3)
). r-values имеют область видимости выражения (уничтожаются в конце выражения, в котором находятся) и им нельзя что-либо присвоить. Этот запрет на присваивание имеет смысл, так как присваивая значение мы вызываем в объекта побочные эффекты.
А поскольку r-values имеют область видимости выражения, то, если бы мы присваивали какое-либо значение для r-value, r-value либо выходило бы из области видимости, прежде чем у нас была бы возможность использовать присвоенное значение в следующем выражении (что делает операцию присваивания бесполезной), либо нам пришлось бы использовать переменную с побочным эффектом, который возникал бы больше одного раза в выражении (что, как вы уже должны знать, привело бы к неопределенным результатам!).
Для поддержки семантики перемещения в C++11 ввели 3 новые категории значений:
pr-values;
x-values;
gl-values.
Их понимание не столь важно в изучении или эффективном использовании семантики перемещения, поэтому в значительной степени мы будем их игнорировать. Однако, если вам интересно, то вы можете изучить их более детально здесь.
Ссылки l-value
До версии C++11 существовал только один тип ссылок, его называли просто — «ссылка». В C++11 этот тип ссылки еще называют «ссылкой l-value». Ссылки l-value могут быть инициализированы только изменяемыми l-values.
Ссылки l-value | Могут ли быть инициализированы? | Могут ли значения изменяться? |
Изменяемые l-values | Да | Да |
Неизменяемые l-values | Нет | Нет |
r-values | Нет | Нет |
Ссылки l-value на константные объекты могут быть инициализированы с помощью как l-values, так и r-values. Однако эти значения не могут быть изменены (константы не изменяют свои значения).
Ссылки l-value на const | Могут ли быть инициализированы? | Могут ли значения изменяться? |
Изменяемые l-values | Да | Нет |
Неизменяемые l-values | Да | Нет |
r-values | Да | Нет |
Ссылки l-value на константные объекты особенно полезны, так как позволяют передавать аргументы любого типа (l-value или r-value) в функцию без выполнения копирования аргумента.
Ссылки r-value
В C++11 добавили новый тип ссылок — ссылки r-value. Ссылки r-value — это ссылки, которые инициализируются только значениями r-values. Хотя ссылка l-value создается с использованием одного амперсанда, ссылка r-value создается с использованием двойного амперсанда:
1 2 3 |
int x = 7; int &lref = x; // инициализация ссылки l-value переменной x (значение l-value) int &&rref = 7; // инициализация ссылки r-value литералом 7 (значение r-value) |
Ссылки r-value не могут быть инициализированы значениями l-values.
Ссылки r-value | Могут ли быть инициализированы? | Могут ли значения изменяться? |
Изменяемые l-values | Нет | Нет |
Неизменяемые l-values | Нет | Нет |
r-values | Да | Да |
И ссылки r-value на константные объекты:
Ссылки r-value на const | Могут ли быть инициализированы? | Могут ли значения изменяться? |
Изменяемые l-values | Нет | Нет |
Неизменяемые l-values | Нет | Нет |
r-values | Да | Нет |
Ссылки r-value имеют два полезных свойства:
Они увеличивают продолжительность жизни объекта, которым инициализируются, до продолжительности жизни ссылки r-value (ссылки l-value на константные объекты также могут это делать).
Неконстантные ссылки r-value позволяют нам изменять значения r-values, на которые указывают ссылки r-value!
Рассмотрим следующую программу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#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() { Fraction &&rref = Fraction(4, 7); // ссылка r-value на анонимный объект класса Fraction std::cout << rref << '\n'; return 0; } // rref (и анонимный объект класса Fraction) выходит из области видимости здесь |
Результат:
4/7
Создаваемый анонимный объект Fraction(4, 7)
обычно вышел бы из области видимости в конце выражения, в котором он определен. Однако, так как мы инициализируем ссылку r-value этим анонимным объектом, то его продолжительность жизни увеличивается до продолжительности жизни самой ссылки r-value, т.е. до конца функции main(). Затем мы используем ссылку r-value для вывода значения анонимного объекта класса Fraction.
Теперь рассмотрим менее наглядный пример:
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> int main() { int &&rref = 7; // поскольку мы инициализируем ссылку r-value литералом 7, то создается временный объект со значением 7, на который указывает ссылка r-value rref = 12; std::cout << rref; return 0; } |
Результат выполнения программы:
12
Хотя это может показаться странным, но при инициализации ссылки r-value литералом, создается временный объект, на который ссылается ссылка r-value (она не ссылается на сам литерал).
Ссылки r-value не очень часто используются так, как это представлено в вышеприведенных примерах.
Ссылки r-value в качестве параметров функции
Ссылки r-value чаще всего используются в качестве параметров функции. Это наиболее полезно при перегрузке функций, когда мы хотим, чтобы выполнение функции отличалось в зависимости от аргументов (l-values или r-values). Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> void fun(const int &lref) // перегрузка функции для работы с аргументами l-values { std::cout << "l-value reference to const\n"; } void fun(int &&rref) // перегрузка функции для работы с аргументами r-values { std::cout << "r-value reference\n"; } int main() { int x = 7; fun(x); // аргумент l-value вызывает функцию с ссылкой l-value fun(7); // аргумент r-value вызывает функцию с ссылкой r-value return 0; } |
Результат выполнения программы:
l-value reference to const
r-value reference
Как вы можете видеть, при передаче l-value, выполняется перегрузка функции с ссылкой l-value в качестве параметра, а при передаче r-value — выполняется перегрузка функции с ссылкой r-value в качестве параметра.
Зачем это может нам понадобиться? Более детально об этом мы поговорим на следующем уроке. Излишне говорить, что это важная часть семантики перемещения.
Возврат ссылки r-value
Вы почти никогда не должны возвращать ссылку r-value из функции по той же причине, по которой вы почти никогда не должны возвращать ссылку l-value из функции. В большинстве случаев вы возвратите висячую ссылку (указывающую на удаленную память), а объект, на который будет ссылаться ссылка, выйдет из области видимости в конце функции.
Тест
Какие из следующих стейтментов, обозначенные буквами, не скомпилируются:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
int main() { int x; // Ссылки l-value int &ref1 = x; // A int &ref2 = 7; // B const int &ref3 = x; // C const int &ref4 = 7; // D // Ссылки r-value int &&ref5 = x; // E int &&ref6 = 7; // F const int &&ref7 = x; // G const int &&ref8 = 7; // H return 0; } |
Ответ
B
, E
и G
не скомпилируются.
Юрій, Дякую Велике Вам, за вашу роботу!!!
Курс по С++, неймовірний )
Пожалуйста))
Т.е. я так понимаю что при передаче r-value в функцию по приоритету вызывается та перегруженная функция, параметры которой принимают ссылку r-value? Ведь принять r-value может и константная ссылка l-value, как в примере
Этот код выдает ошибку. То есть, ссылка переноса — lvalue?
Абсолютно верно. Сама r-value reference, хотя и ссылается на объекты r-value, является именованным НЕ ВРЕМЕННЫМ объектом в памяти, а значит, l-value.
Ох , я чего-то не понимаю.
В разделе ссылки на L-values вторая строка первой таблицы правильно заполнена?
Если да, то получается type const& VAR = value; — не скомпилируется?
Или я о чем-то не том думаю?
Вторая строка первой таблицы заполнена правильно, Вы ведь про константную ссылку l-value пишете? Тогда надо смотреть вторую таблицу, константная ссылка l-value может быть инициализирована чем угодно, но не может потом меняться. В 89 уроке было: "ссылки на константные значения могут быть инициализированы неконстантными l-values, константными l-values и r-values". Поэтому пример С:
— скомпилируется, но нельзя будет потом:
т.к. в том же 89 уроке было: "Как и в случае с указателями, константные ссылки также могут ссылаться и на неконстантные переменные. При доступе к значению через константную ссылку, это значение автоматически считается const, даже если исходная переменная таковой не является"
А почему скомпилируется D?
Простая константная ссылка, может указывать на r-values, посмотри по таблице….
Здравствуйте! Спасибо Вам огромное за проделанную работу! Я хотел поинтересоваться: Вы собираетесь вести какой-то другой туториал после того, как закончите перевод материала по c++?
Привет. Спасибо, что читаешь 🙂 Честно — сам ещё не знаю, нужно этот цикл уроков допереводить, а там уже будет видно.