Форум программистов, компьютерный форум, киберфорум
Наши страницы
outoftime
Войти
Регистрация
Восстановить пароль
Рейтинг: 5.00. Голосов: 1.

Как понять rvalue & lvalue в C/C++ (перевод)

Запись от outoftime размещена 12.02.2018 в 22:49
Обновил(-а) outoftime 16.02.2018 в 21:32 (Правки от John C Reynolds)

Термины lvalue и rvalue не те с которыми приходится часто сталкиваться работая с С/С++, но когда приходится, не совсем ясно что именно они значат. Чаще всего, их можно увидить при компиляции кода, когда компилятор сообщает об ошибках или выдает предупреждения. К примеру, компилируя следующий код с помощь gcc

C++
1
2
3
4
5
6
7
8
int foo() { return 2; }
 
inf main() 
{
    foo() = 2;
 
    return 0;
}
Мы получим:

test.c: In function 'main':
test.c:8:5: error: lvalue required as left operand of assignment
И да, да, этот код писали какие-то извращенцы и Вы точно так не делаете, но в сообщениях об ошибках упоминается lvalue, этот термин не является тем, которые часто встречаются в учебниках по C/C++. Рассмотрим еще один пример, модифицировав немного foo() и выполнив компиляцию уже с g++:

C++
1
2
3
4
int& foo() 
{ 
    return 2; 
}
Сейчас, мы получили следующие ошибки:
testcpp.cpp: In function 'int& foo()':
testcpp.cpp:5:12: error: invalid initialization of non-const reference
of type 'int&' from an rvalue of type 'int'
Вот, снова, в ошибках упоминается мистический rvalue. Так что же значат rvalue и lvalue в C и C++? Об этом пойдет речь далее.

Простое определение


Эта глава содержит намеренно упрощенные определения lvalue и rvalue. Остаток статьи будет дополнять эти определения.

lvalue (с англ. "locator value" - значение экземпляра) представляет объект который размещается в некоторой опознаваемой области памяти (т.е. имеет адрес).

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

Базовые примеры


Определения терминов выше могут казаться расплывчатыми, именно поэтому важно посмотреть на некоторые примеры.

Предположим, у нас определена переменная целочисельного типа которой присвоено значение:

C++
1
2
int var;
var = 4;
Оператор присвоения ожидает lvalue в качестве левого операнда, и var это lvalue, так как это объект с опознаваемым расположением в памати. Но, с другой стороны, следующие примеры недействительные:

C++
1
2
4 = var;       // Ошибка
(var + 1) = 4; // Ошибка
Ни константа 4, не выражение var + 1 не являются lvalue (а, следовательно, это rvalue). Они не lvalue потому что оба являются временными результатами выражений, которые не имеют опознаваемого положения в памяти (т.е. они могут просто располагаться в некотором временном регистре на период вычислений). Таким образом, присвоение им не имеет никакого смысла - некуда присваивать значение.

И так, сейчас уже должно быть ясно что значат ошибки из cамых первых примеров. foo возвращает временное значение, которое является rvalue. Попытка присвоить ему значение приводит к ошибке, когда компилятор смотрит на foo() = 2; он говорит что ожидается lvalue в качестве левой части выражения присвоения.

Тем не менее, не все присвоения к результатам вызовов функций недействительные. К примеру, ссылка в C++ делает это возможным:

C++
1
2
3
4
5
6
7
8
9
10
11
12
int globalvar = 20;
 
int& foo()
{
    return globalvar;
}
 
int main() 
{
    foo() = 10;
    return 0;
}
На этом примере, foo возвращает ссылку, которая является lvalue, таким образом к ней можно применять оператор присвоения. На самом деле, возможность С++ возвращать lvalue из функций важно для реализации некоторых перегрузок операторов. Один из общих примеров это перегрузка оператора обращения по индексу operator [] в классах которые имеют некоторый вид поиска по индексу. std::map делает следующее:

C++
1
2
std::max<int, float> mymap;
mymap[10] = 5.6;
Присвоение mymap[10] работает из-за неконстантной перегрузки std::map::operator[] возвращающего ссылку к которой может быть применен оператор присвоения.

Изменяемые lvalue


Изначально, когда было определение для С, оно буквально имело смысл "значения пригодные для применения в левой части выражений присвоения". Однако, позже, когда в ISO C было добавлено ключевое слово const, это определение должно было быть откоректоровано. В конце концов:

C++
1
2
const int a = 10; // 'a' это lvalue
a = 10;           // но ему нельзя присваивать значения!
Поэтому соответствующие корективы должны были быть добавлены. Не всем lvalue можно присваивать значения. Тех, кому можно, называют изменяемые lvalue. Оффициально, стандарт С99 определяет изменяемые lvalue как:

Цитата:
[...] lvalue которые не имеют тип массив, не имеют неполный тип, не являются константными и, если это структура или объеденение (union), не имеют полей (включая рекурсивные) которые помечены как константные.
Заметка от автора: я пытался найти эти строки в стандарте, в 13.02.2018 но точно такой же фразы не нашел (может плохо искал, кто знает). Если найдете, дайте ссылку в комментах.

Преобразования lvalue в rvalue


Грубо говоря, языковые конструкции, оперирующие значениями объектов, требуют rvalue в качестве аргументов. Например, оператор сложения "+" принимает два rvalue в качестве аргументов и возвращает rvalue:

C++
1
2
3
4
int a = 1;     // a это lvalue 
int b = 2;     // b это lvalue
int c = c + b; // + требует rvalue, поэтому a и b преобразовываются в rvalue
               // и возвращаемое значение также rvalue
Как видим, a и b оба lvalue. Таким образом, в третьей строке они претерпевают неявное преобразование lvalue к rvalue. Таким образом, все lvalue, которые не являются массивами, функциями или неполными типами могут быть преобразованы в rvalue.

Что на счет обратного преобразования? Могут ли rvalue быть преобразованными в lvalue? Конечно же нет! Это нарушит саму природу lvalue в соответствии c определением. [1]

Это не значит нельзя произвести lvalue из rvalue более явными средствами. К примеру, унарный оператор "*" (разименование) принимает rvalue в качестве аргумента, но производит lvalue в качестве результата. Рассмотрим следующий код:

C++
1
2
3
int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10;     // Всё хорошо: p + 1 это rvalue, но *(p + 1) это lvalue
Опять таки, унарный оператор взятия ссылки "&" принимает lvalue в качестве аргументов и возвращает rvalue:

C++
1
2
3
4
int var = 10;
int* bad_addr = &(var + 1); // Ошибка: lvalue требуется в качестве "&" операнда
int* addr = &var;           // Всё хорошо: var это lvalue
&var = 40;                  // Ошибка: lvalue требуется в качестве левого операнда присвоения
Амперсант играет другую роль в С++ - он позволяет определять ссылки. Они называются "lvalue ссылки". Оператор присвоения не может быть вызван для не константной lvalue ссылки, так как это потребует недействительного преобразования rvalue в lvalue:

C++
1
2
3
std::string& sref = std::string(); // Ошибка: недействительная инициализация
                                   // не константногой ссылки типа "std::string&"
                                   // из rvalue типа "std::string"
Константным lvalue ссылкам может быть присоено rvalue значение. Так как они константные, значение не может изменяться через ссылку и, следовательно, нету проблемы с изменением rvalue. Это делает возможным существование очень общей С++ идиомы принятия значений по константной ссылке в функцию, которая избегает ненужного копирования и создания временных объектов.

CV-классифицированные rvalue (CV-qualified rvalues)


Если мы будем внимательно читать часть стандарта C++ обсуждающего преобразование lvalue в rvalue, мы заметим следующее:

Цитата:
lvalue (3.10) не функции, не массива типа T может быть сконвертирован в rvalue. [...] Если T не является типом класса, тип rvalue является cv-неклассифицированной версией T. В противном случае, тип rvalue - T.
Что значит "cv-неклассифицированный"? CV-класссификатор (CV - "const volatile", если в двух словах) это термин использующийся для описания const и volatile классификаторов типов.

Из секции 3.9.3:

Цитата:
Каждый тип который является cv-неклассифицированным полным или неполным объектом типа или void (3.9) имеет три соответсвующие cv-классифицированные версии своего типа: const-классифицированная, volatile-классифицировання и const-volatile-классифицированная версии. [...] cv-классифицированная или cv-неклассифицированная версии типа являются отдельными типами; однако, одни должны иметь тоже представление и требования к выравниванию (3.9)
Вы наверное уже задались вопросом "Какое отношение это всё имеет к rvalue?". Дело в том, что в Си, rvalue никогда не имеют cv-классифицированных типов. Только lvalue имеют. C другой стороны, в C++, класс rvalue может быть cv-классифицированным типом, но встроенные типы данных (такие как int) не могут. Рассмотрим пример:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
 
class A {
  public:
    void foo() const { std::cout << "A::foo() const\n"; }
    void foo() { std::cout << "A::foo()\n"; }
};
 
A bar() { return A(); }
const A cbar() { return A(); }
 
 
int main()
{
    bar().foo();   // вызывает foo
    cbar().foo();  // вызывает foo const
}
Второй вызов в main и в самом деле вызывает метод foo() const класса A, потому что тип возвращаемый cbar это const A, который отличается от A. Это в точности то что имелось в виду в последнем предложении цитаты упомянутой ранее. Заметим также что возвращаемое значение cbar это rvalue. Таким образом это живой пример cv-классифицированного rvalue.

rvalue ссылки (C++11)


rvalue ссылки и соответствующая move семантика это одна из наимолее мощных новых возможностей стандарта C++11 представленных в языке. Развёрнутая дискусия по возможностям выходит за границы этой статьи, но мне всё же хочется предоставить простые примеры, потому что, я считаю что это хорошая тема для демонстрации того как понимание lvalue и rvalue помогают нам рассуждать о нетривиальных концепциях языка.

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

В качестве примера, рассмотрим упрощенную реализацию динамического "вектора целых чисел". Я покажу только интересующие методы:

C++
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Intvec
{
  public:
    explicit Intvec(size_t num = 0)
        : m_size(num), m_data(new int[m_size])
    {
        log("constructor");
    }
 
    ~Intvec()
    {
        log("destructor");
        if (m_data)
        {
            delete[] m_data;
            m_data = 0;
        }
    }
 
    Intvec(const Intvec &other)
        : m_size(other.m_size), m_data(new int[m_size])
    {
        log("copy constructor");
        for (size_t i = 0; i < m_size; ++i)
            m_data[i] = other.m_data[i];
    }
 
    Intvec &operator=(const Intvec &other)
    {
        log("copy assignment operator");
        Intvec tmp(other);
        std::swap(m_size, tmp.m_size);
        std::swap(m_data, tmp.m_data);
        return *this;
    }
 
  private:
    void log(const char *msg)
    {
        cout << "[" << this << "] " << msg << "\n";
    }
 
    size_t m_size;
    int *m_data;
};
И так, у нас имеются объявленные: обычный конструктор, деструктор, конструктор копирования (copy constructor) и оператор присваения копированием (copy assignment operator), все используют метод логирования, чтобы мы могли видеть когда они вызываются.

Давайте выполним простой код, который копирует содержимое v1 в v2:

C++
1
2
3
4
5
6
Intvec v1(20);
Intvec v2;
 
cout << "assigning lvalue...\n";
v2 = v1;
cout << "ended assigning lvalue...\n";
Результат будет, примерно, следующим:

assigning lvalue...
[0x28fef8] copy assignment operator
[0x28fec8] copy constructor
[0x28fec8] destructor
ended assigning lvalue...


Вполне логично - логирование вполне честно показывает нам что происходит внутри оператора присваивания (operator=). Предположим, мы хотим присвоить некоторое значение rvalue переменной v2:

C++
1
2
3
cout << "assigning rvalue...\n";
v2 = Intvec(33);
cout << "ended assigning rvalue...\n";
Не смотря на то, что я просто присваиваю только что созданный вектор, это всего лишь демонстрация более обобщенного случая, когда некоторое временное значение rvalue создается, а потом присваевается переменной v2 (например, такое может произойти когда функция возвращает вектор). Этот пример выведет следующее:

assigning rvalue...
[0x28ff08] constructor
[0x28fef8] copy assignment operator
[0x28fec8] copy constructor
[0x28fec8] destructor
[0x28ff08] destructor
ended assigning rvalue...


Явно выполняются лишние операции. В частности, имеется одна лишняя пара вызовов конструктра и деструктора для создания и, затем, удаления временного объекта. За такое должно быть стыдно, потому что внутни оператора присвоения копированием, создается и уничтожается временная копия. Эти накладные расходы не приносят пользы.

Испугались? C++11 предоставляет нам ссылки на rvalue с помощь которых мы можем реализовать "move семантику", и, в частности, "оператор присваения перемещением" ("move assignment operator"). Добавим еще один operator= к нашему классу Intvec:

C++
1
2
3
4
5
6
7
Intvec& operator=(Intvec&& other)
{
    log("move assignment operator");
    std::swap(m_size, other.m_size);
    std::swap(m_data, other.m_data);
    return *this;
}
Знак && указывает на новую rvalue ссылку. Он делает в точности то, что и должен делать - дает нам ссылку на rvalue, которое будет уничножено после завершения вызова. Мы можем использовать этот факт чтобы "украсть" внутренности rvalue - они всё равно уже не понадобятся! Сейчас мы имеем следующий вывод:

assigning rvalue...
[0x28ff08] constructor
[0x28fef8] move assignment operator
[0x28ff08] destructor
ended assigning rvalue...


Так как v2 присваевается значение rvalue, вызывается только что созданный оператор присваения пермещением. Вызовы конструктора и деструктора всё еще нужны для временного объекта, созданного Intvec(33), но в другом временном объекте внутри оператора присваения больше нет надобности. Оператор просто меняет местами внутренний буфер rvalue со своим собственным, устраивая всё так, что деструктор rvalue займется чисткой нашего буфера, который больше не будет использоваться. Красота.

Хочу еще раз упомянуть, что этот пример всего лишь верхушка айсберга "move семантики" и ссылок rvalue. Как Вы уже, наверное, догадались, это сложная тема с множеством особых случаев и подводных камней для рассмотрения. Моей целью является демонстрация очень интересных применений разницы между lvalue и rvalue в C++. Компилятор, очевидно, знает когда некая сущность является rvalue и может вызывать соответсвующий конструктор во время компиляции.

Перевод выполнил: Руслан Ковтун
Дата публикации оригинальной статьи: 12.02.2011
Ссылка: https://eli.thegreenplace.net/2011/1...ues-in-c-and-c
Размещено в C/C++
Просмотров 483 Комментарии 0
Всего комментариев 0
Комментарии
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.
Рейтинг@Mail.ru