Ми вже раніше розглядали 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 не скомпілюються.
