Пока я знакомился с Modern C++. Большую часть времени я был поражен компилятором С++, говорящим, что это lvalue, а это жаргон rvalue. И даже если вы не используете C++, вы можете столкнуться с ошибкой компилятора языка C, говорящей "lvalue требуется как левый операнд присваивания". Поэтому у меня возникли трудности с пониманием этой необученной темы lvalue rvalue и их ссылки с примерами на C++, пока я немного не поискал в Google то же самое. Это моя привычка сохранять знания в рамках статьи. Итак, вот немного о вещах, которые я узнал до сих пор.

У меня всегда одна мысль перед тем, как я узнаю что-то новое: «Зачем нам это нужно?» Итак, давайте начнем с него.

/!\: Кстати, это кросс-пост из моего блога.

Зачем нам нужны жаргоны типа lvalue и rvalue?

  • Если вы используете C++ до C++11, вам не нужны эти жаргоны для написания кода. Но да, все же полезно понимать ошибки компиляции.
  • Компилятор видит вещи по выражению и оценивает выражение, которое идентифицирует операнд и операцию. Давайте разберем это на примере:
uint32_t a = 5;
  • Здесь компилятор идентифицирует a и 5 как операнд, а = (присваивание) как операцию. Кроме того, компилятор делит операнды на подкатегории с именами rvalues, lvalues, xvalues, glvalues и prvalues, чтобы различать их (см. иерархию в заголовке image). Этот другой тип значения сообщает компилятору об источнике, назначении, объеме информации и т. д.
  • В приведенном выше выражении a является значением lvalue, поскольку оно указывает целевую память, где будет храниться значение rvalue, т. е. 5.
  • Когда вы скомпилируете и увидите приведенный выше оператор в сборке, он, вероятно, будет выглядеть так:
...
movl    $5, -4(%ebp)
...
  • Здесь (%ebp) — это указатель текущего кадра, который тянет вниз на 4 байта, что указывает на пространство, выделенное компилятором для переменной в стеке. И инструкция movl сохраняет 5 непосредственно в этой ячейке памяти.
  • Это прямолинейно, пока мы используем примитивные типы данных, такие как int, double, char и т. д. Таким образом, компилятор будет хранить необработанное значение непосредственно в самом коде инструкции, как в нашем случае это $5. После выполнения этой инструкции $5 не используется, поэтому имеет область выражения, другими словами, является временной.
  • Но когда мы используем класс и структуру, которые являются определяемым пользователем типом, все становится немного сложнее, и компилятор вводит временный объект вместо того, чтобы напрямую сохранять значение в самом коде инструкции.

lvalue rvalue и их ссылки с примером

Что такое lvalue и rvalue?

  • lvalue и rvalue — это идентификаторы компилятора для оценки выражения.
  • Любой идентификатор компилятора, представляющий расположение в памяти, является lvalue.
  • Любой идентификатор компилятора, который представляет значение данных в правой части оператора присваивания (=), является rvalue.

Примеры lvalue

  • Существует два типа изменяемого и немодифицируемого значения lvalue (это const).

1. изменяемое lvalue

2. неизменяемое lvalue

Примеры значения

  • rvalue может быть функцией справа от оператора присваивания =, которая в конечном итоге оценивается как объект (примитивный или определяемый пользователем).
  • rvalue обычно оцениваются по своим значениям, имеют область выражения (они умирают в конце выражения, в котором они находятся) большую часть времени и не могут быть назначены. Например:
5 = a; // invalid
getInt() = 2; // invalid

lvalue rvalue ссылки с примером

ссылка на lvalue

  • Ссылка lvalue — это ссылка, которая привязывается к lvalue.
  • Ссылки lvalue помечаются одним амперсандом &.
int x = 5;
int &lref = x; // lvalue reference initialized with lvalue x
  • До C++11 в C++ существовал только один тип ссылки, поэтому он назывался просто «ссылка». Однако в C++11 его иногда называют ссылкой lvalue.
  • Ссылки lvalue могут быть инициализированы только модифицируемыми lvalue.
const int a = 5;
int &ref = a; // Invalid & error will be thrown by compiler

Исключение

  • Мы не можем привязать ссылку lvalue к rvalue
int &a = 5; // error: lvalue cannot be bound to rvalue 5

Однако мы можем привязать rvalue к ссылке const lvalue (константная ссылка):

const int &a = 5;  // Valid
  • В этом случае компилятор сначала преобразует 5 в lvalue, а затем присваивает место в памяти константной ссылке.

ссылка на rvalue

  • Это, безусловно, самая полезная и немного сложная вещь, которую вы узнаете.
  • Ссылка rvalue — это ссылка, которая привязывается к rvalue. Ссылки rvalue помечаются двумя амперсандами &&.
int &&rref = 5; // rvalue reference initialized with rvalue 5
  • Ссылки rvalue не могут быть инициализированы с помощью lvalue, т.е.
int a = 5;
int &&ref = a; // Invalid & error will be thrown by compiler
  • Ссылки rvalue чаще используются в качестве параметров функции. Это наиболее полезно для перегрузок функций, когда вы хотите иметь различное поведение для аргументов lvalue и rvalue.
void fun(const int &lref) // lvalue arguments will select this function
{
    std::cout << "lvalue reference to const\n";
}
void fun(int &&rref) // rvalue arguments will select this function
{
    std::cout << "rvalue reference\n";
}
int main()
{
    int x = 5;
    fun(x); // lvalue argument calls lvalue version of function
    fun(5); // rvalue argument calls rvalue version of function
    return 0;
}

Зачем нам нужны ссылки на rvalue?

  • Если вы наблюдаете за прототипом копирующий конструктор и копирующий оператор присваивания, он всегда принимает const ссылочный объект в качестве аргумента. Потому что их основная работа заключается в копировании объекта. И при копировании мы не хотим изменять объект, который мы предоставили справа.
  • Но есть некоторые сценарии, когда нас не волнует правый объект, который мы предоставили для копирования. Например:
class IntArray{
    int *m_arr;
    int m_len;
public:
    IntArray(int len) : m_len(len), m_arr(new int[len]){}
    ~IntArray(){delete [] m_arr;}
    // Copy Constructor
    IntArray(const IntArray& rhs){
      m_arr = new int[rhs.m_len];
      m_len = rhs.m_len;
      for(int i=0;i<m_len;i++)
        m_arr[i] = rhs.m_arr[i];
    }
};
IntArray func()
{
    IntArray obj(5);    
    // process obj    
    return obj;
}
int main()
{
  IntArray arr = func();   
  return 0;
}
// Note: use "-fno-elide-constructors" option while compiling otherwise it will create copy elision
  • Наблюдая за этим кодом, мы заключаем, что obj бесполезна после возврата функции func(). Но когда вы возвращаете объект по значению, он вызывает конструктор копирования, который копирует все содержимое из obj в arr (объявленное в main()), выделяя новый ресурс для arr. И когда obj выходит за рамки, он освобождает свои ресурсы.
  • Вместо того, чтобы выделять новые ресурсы и копировать в них данные, почему бы нам просто не использовать существующие ресурсы obj? Давайте сделаем это:

конструктор перемещения

IntArray(IntArray&& rhs){
    m_arr = rhs.m_arr;
    m_len = rhs.m_len;
    rhs.m_arr = nullptr; // To prevent code crashing 
}
  • Я только что изменил код конструктора копирования, как указано выше, который принимает ссылку rvalue в качестве аргумента, а не lvalue, так что наш перегруженный конструктор копирования будет вызываться только тогда, когда в правой части используется значение rvalue.
  • Это просто означает, что этот конструктор будет вызываться только тогда, когда правый объект является временным или программист больше не заботится об этом объекте.
  • Реализация просто стала владельцем ресурсов от obj до arr и установила указатель объекта в правой части на NULL, чтобы его деструктор не освободил ресурс, которым он больше не владеет.
  • На самом деле это конструктор перемещения, а не конструктор копирования. Чья основная задача состоит в том, чтобы взять/переместить право собственности на ресурсы.
  • Рассмотрим следующий прототип конструктора перемещения для более глубокого понимания:

Получение ссылки на rvalue

IntArray(IntArray&& rhs)
{
    ...
}
  • Сообщение этого кода таково: «Объект, к которому привязывается rhs, является ВАШИМ. Делайте с ним что хотите, все равно никому не будет дела». Это похоже на передачу копии IntArray, но без создания копии.

Зачем нам нужен конструктор перемещения?

Это может быть интересно по двум причинам:
— повышение производительности (поскольку мы не выделяем новые ресурсы и не передаем содержимое).
— получение права собственности (поскольку объект, к которому привязана ссылка, был заброшен вызывающей стороной). ).

  • Я знаю, вы можете подумать, почему бы нам просто не изменить конструктор копирования, удалив из него ключевое слово const. Сделаем так же
IntArray(IntArray& rhs){
}
  • Ошибка компиляции
exit status 1
error: no matching constructor for initialization of 'IntArray'
  IntArray arr = func();
           ^     ~~~~~~
note: candidate constructor not viable: expects an lvalue for 1st argument
    IntArray(IntArray& rhs){
    ^
1 error generated.
  • Если вы видите note выше, наш перегруженный конструктор копирования запрашивает lvalue. Что мы делаем, так это предоставляем rvalue. Когда мы возвращаем объект по значению, временный объект (подпадающий под категорию rvalue) будет создан и передан нашему конструктору копирования. И, как мы уже видели выше, ссылка lvalue не может быть привязана к объекту rvalue.
  • Не думайте об изменении аргумента вашего конструктора копирования как ссылки const lvalue, я знаю, что мы видели это исключение, и мы можем привязать ссылку const lvalue к rvalue/временному объекту. Но в этом случае вы не можете перемещать/переносить ресурс, так как он const.

Итак, это все для lvalue rvalue и их ссылки с примером, в следующей статье мы разработаем умный указатель, используя ссылку rvalue и другие концепции, полученные здесь.

Тебе может понравиться