M.2 – Ссылки на r-значения (rvalue-ссылки)

Добавлено 17 сентября 2021 в 22:57

Еще в главе 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 не компилируются.

Теги

C++ / Cppl-value / l-значениеLearnCppr-value / r-значениеДля начинающихОбучениеПрограммированиеСемантика перемещенияСсылка / Reference (программирование)

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.