Value c что это
Перейти к содержимому

Value c что это

  • автор:

Понимание lvalue и rvalue в C и С++

Привет, Хабр! Представляю вашему вниманию перевод статьи Eli Bendersky, Understanding of lvalues and rvalues in C and C++.

От переводчика: предлагаю Вашему вниманию перевод интересной статьи об lvalue и rvalue в языках C/C++. Тема не нова, но знать об этих понятиях никогда не поздно. Статья рассчитана на новичков, либо на программистов переходящих с C (или других языков) на C++. Поэтому будьте готовы к подробному разжёвыванию. Если вам интересно, добро пожаловать под кат

Термины lvalue и rvalue не являются чем-то таким, с чем часто приходится сталкиваться при программировании на C/C++, а при встрече не сразу становится ясным, что именно они означают. Наиболее вероятное место столкнуться с ними — это сообщения компилятора. Например, при компиляции следующего кода компилятором gcc :

int foo() < return 2; >int main()

Вы уведите нечто следующее:

test.c: In function 'main': test.c:8:5: error: lvalue required as left operand of assignment 

Согласен, что этот код немного надуманный, и вряд ли Вы будете писать нечто подобное, однако сообщение об ошибке упоминает lvalue — термин, который не так часто увидишь в туториалах по C/C++. Другой пример нагляден при компиляции следующего кода при помощи g++ :

int& foo()

Вы увидите следующую ошибку:

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. Что же в C и C++ понимается под lvalue и rvalue? Это и есть тема данной статьи.

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

Для начала нарочито дадим определения lvalue и rvalue в упрощённой форме. В дальнейшем эти понятия будут рассмотрены под увеличительным стеклом.

lvalue (locator value) представляет собой объект, который занимает идентифицируемое место в памяти (например, имеет адрес).

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

Элементарные примеры

Термины, определённые выше, могут показаться немного нечёткими. Поэтому стоит сразу рассмотреть несколько простых поясняющих примеров. Предположим, мы имеем дело с переменной целого типа:

int var; var = 4; 

Оператор присваивания ожидает lvalue с левой стороны, и var является lvalue, потому что это объект с идентифицируемым местом в памяти. С другой стороны, следующие заклинания приведут к ошибкам:

4 = var; // ERROR! (var + 1) = 4; // ERROR! 

Ни константа 4 , ни выражение var + 1 не являются lvalue
(что автоматически их делает rvalue). Они не lvalue, потому что оба являются временным результатом выражений, которые не имеют определённого места в памяти (то есть они могут находится в каких-нибудь временных регистрах на время вычислений). Таким образом, присваивание в данном случае не несёт в себе никакого семантического смысла. Иными словами — некуда присваивать.

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

Однако, не все присваивания результату вызова функции ошибочны. Например, использование ссылок в C++ делает это возможным:

int globalvar = 20; int& foo() < return globalvar; >int main()

Здесь foo возвращает ссылку, которая является lvalue, то есть ей можно придать значение. Вообще, в C++ возможность возвращать lvalue, как результат вызова функции, существенна для реализации некоторых перегруженных операторов. Как пример приведём перегрузку оператора [] в классах, которые реализуют доступ по результатам поиска. Например std::map :

std::map mymap; mymap[10] = 5.6; 

Присваивание mymap[10] работает, потому что неконстантная перегрузка std::map::operator[] возвращает ссылку, которой может быть присвоено значение.

Изменяемые lvalue

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

const int a = 10; // 'a' - lvalue a = 10; // но ему не может быть присвоено значение! 

Таким образом не всем lvalue можно присвоить значение. Те, которым можно, называются изменяемые lvalue (modifiable lvalues). Формально C99 стандарт определяет изменяемые lvalue как:

[. ] lvalue, тип которого не является массивом, не является неполным, не имеет спецификатор const , не является структурой или объединением, содержащими поля (также включая поля, рекурсивно вложенные в содержащиеся агрегаты и объединения) со спецификатором const .

Преобразования между lvalue и rvalue

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

int a = 1; // a - lvalue int b = 2; // b - lvalue int c = a + b; // '+' требует rvalue, поэтому a и b конвертируются в rvalue // и rvalue возвращается в качестве результата 

Как мы уже видели раньше, a и b оба lvalue. Поэтому в третьей строке они подвергаются неявному преобразованию lvalue-в-rvalue. Все lvalue, которые не являются массивом, функцией и не имеют неполный тип, могут быть преобразованы в rvalue.

Что насчёт преобразования в другую сторону? Можно ли преобразовать rvalue в lvalue? Конечно нет! Это бы нарушило суть lvalue, согласно его определению (Отсутствие неявного преобразования означает, что rvalue не могут быть использованы там, где ожидается lvalue).

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

int arr[] = ; int* p = &arr[0]; *(p + 1) = 10; // OK: p + 1 rvalue, однако *(p + 1) уже lvalue 

Обратно, унарный оператор ‘&’ (адрес) принимает lvalue как аргумент и производит rvalue:

int var = 10; int* bad_addr = &(var + 1); // ОШИБКА: требуется lvalue для унарного оператора '&' int* addr = &var; // ОК: var - lvalue &var = 40; // ОШИБКА: требуется lvalue с левой стороны // оператора присваивания 

Символ «&» играет несколько другую роль в C++ — он позволяет определить ссылочный тип. Его называют «ссылкой на lvalue». Неконстантной ссылке на lvalue не может быть присвоено rvalue, так как это потребовало бы неверное rvalue-в-lvalue преобразование:

std::string& sref = std::string(); // ОШИБКА: неверная инициализация // неконстантной ссылки типа 'std::string&' // rvalue типа 'std::string' 

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

CV-специфицированные rvalues

Если прочесть внимательно часть С++ стандарта, касающуюся преобразования lvalue-в-rvalue (глава 4.1 в драфте стандарта C++11), то можно увидеть следующее:

lvalue (3.10) на тип T, не являющимся функциональным, или массивом, может быть преобразован в rvalue. [. ] Если T не класс, типом rvalue является cv-неспецифицированная версия типа T. Иначе, типом rvalue является T.

Так что же значит «cv-неспецифицированный»? CV-спецификатор — это термин, используемый для описания const и volatile спецификаторов типа.

Каждый тип, который является cv-неспецифицированным полным или неполным объектным типом или типом void (3.9), имеет соответственно три cv-специфицированные версии: тип со спецификатором const, тип со спецификатором volatile и тип со спецификаторами const volatile. [. ] CV-специфицированные и cv-неспецифицированные типы являются различными, однако они имеют одинаковое представление и требования по выравниванию.

Но как всё это связано с rvalue? В языке C rvalue никогда не имеют cv-специфицированных типов. Это свойство lvalue. Однако в C++ классовые rvalue могут быть cv-специфицированным, что не касается встроенных типов вроде int . Рассмотрим пример:

#include class A < public: void foo() const < std::cout void foo() < std::cout >; A bar() < return A(); >const A cbar() < return A(); >int main() < bar().foo(); // вызовет foo cbar().foo(); // вызовет foo const >

Вторая строчка в функции main вызовет метод foo() const , так как cbar возвращает объект типа const A , который отличен от A . Это как раз то, что имелось в виду в последнем предложении выдержки из стандарта выше. Кстати, заметьте, что возвращаемое cbar значение является rvalue. Это был пример cv-специфицированных rvalue в действии.

Ссылки на rvalue (C++11)

Ссылки на rvalue и сопутствующий концепт семантики переноса являются одним из наиболее мощным инструментом, добавленным в язык C++11. Подробная дискуссия на эту тему выходит за рамки этой скромной статьи (вы можете найти кучу материала, просто погуглив «rvalue references». Вот некоторые ресурсы, которые я нахожу полезными: этот, этот и особенно вот этот), но всё же я хотел бы привести простой пример, потому что считаю, что данная глава является наиболее подходящим место, чтобы продемонстрировать как понимание lvalue и rvalue расширяет наши возможности рассуждать о нетривиальных концепциях языка.

Добрая половина статьи была потрачена на объяснение того, что одним из самых главных различий между lvalue и rvalue является тот факт, что lvalue можно изменять, в то время как rvalue — нет. Что же, C++11 добавляет одну важнейшую характерную особенность в этом различии, разрешая нам иметь ссылки на rvalue и тем самым изменять их в некоторых случаях.

Как пример рассмотрим наипростейшую реализацию динамического массива целых чисел. Давайте посмотрим лишь на методы относящиесе к теме данной главы:

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 size_t m_size; int* m_data; >; 

Итак, здесь присутствуют обычные конструктор и деструктор, конструктор копирования и оператор присваивания (это каноническая реализация копирующего оператора присваивания с точки зрения усточивости к исключениям. Используя конструктор копирования и затем не выбрасывающий исключения std::swap , мы можем быть уверены, что не может возникнуть промежуточного состояния с непроинициализированной памятью, если где-то произойдёт исключение). Все они используют функцию логирования, чтобы мы могли понять, когда они на самом деле вызваны.

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

Intvec v1(20); Intvec v2; cout  

И вот, что мы увидим:

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

Что совершенно логично, так как это точно отражает, что происходит внутри оператора присваивания. Но давайте предположим, что мы хотим присвоить v2 некоторое rvalue:

cout  

Хотя здесь я только присваиваю значение свеже созданному вектору, это является одной из демонстраций общего случая, когда некоторое временное rvalue создаётся и присваивается v2 (это может случится например, если функция возвращает вектор). Вот что мы увидим на экране:

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

Ого! Выглядит очень хлопотно. В частности, здесь потребовалась дополнительная пара вызовов конструктора с деструктором, чтобы создать, а потом удалить временный объект. И это печально, так как внутри копирующего оператора присваивания другой временный объект создаётся и удаляется. Дополнительная работа за зря.

Но, нет! C++11 даёт нам ссылки на rvalue, с помощью которых можно реализовать «семантику переноса», а в частности «переносящий оператор присваивания» (теперь понятно почему я всё время называл operator= копирующим оператором присваивания. В C++11 эта разница становится важной). Давайте добавим другой operator= в IntVec :

Intvec& operator=(Intvec&& other)

Двойной асперсанд — это ссылка на rvalue. Он означает как раз то, что и обещает — даёт ссылку на rvalue, который будет уничтожен после вызова. Мы можем использовать этот факт, чтобы просто «стащить» внутренности rvalue — они ему всё равно не нужны! Вот, что выведется на экран:

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

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

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

Заключение

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

В стандарте C++11 эта тема является ещё более важной, так как C++11 вводит понятия ссылок на rvalue и семантики переноса. Чтобы действительно понять новые особенности языка, строгое понимание rvalue и lvalue просто необходимо.

Value c что это

В C++ все переменные являются l-values. l-value (в переводе «л-значение», произносится как «ел-валью») – это значение, которое имеет свой адрес в памяти. Поскольку все переменные имеют адреса, то они все являются l-values (например: переменные a, b, c – все являются l-values). l от слова «left», так как только значения l-values могут находиться в левой стороне в операциях присваивания (в противном случае мы получим ошибку). Например, выражение 9 = 10; вызовет ошибку компилятора, так как 9 не является l-value. Число 9 не имеет своего адреса в памяти и, таким образом, мы ничего не можем ему присвоить (9 равно 9, и ничего здесь не изменить).

Противоположностью l-value является r-value (в переводе «р-значение», произносится как «ер-валью»). r-value – это значение, которое не имеет постоянного адреса в памяти. Примерами могут быть единичные числа (например, 7, которое вычисляется в 7) или выражения (например, x + 3, которое вычисляется в значение "х плюс 3").

Вот несколько примеров операций присваивания с использованием r-values:

int a; // объявляем целочисленную переменную a

a = 5; // 5 вычисляется в 5, которое затем присваивается переменной а

a = 4 + 6; // 4 + 6 вычисляется в 10, которое затем присваивается переменной а

int b; // объявляем целочисленную переменную b

b = a; // a вычисляется в 10 (с предыдущих операций), которое затем присваивается переменной b

b = b; // b вычисляется в 10, которое затем присваивается переменной b (ничего не происходит)

b = b + 2; // b + 2 вычисляется в 12, которое затем присваивается переменной b

Давайте детальнее рассмотрим последнюю операцию присваивания:

Здесь переменная b используется в двух различных контекстах. Слева b используется как l-value (переменная с адресом в памяти), справа b используется как r-value и производит отдельное значение (в данном случае 12). При выполнении этого стейтмента, компилятор видит следующее:

И здесь уже становится понятным, какое значение присваивается переменной b.

Всё, что нужно запомнить – это то, что в левой стороне операции присваивания всегда должно находиться l-value (которое имеет свой адрес в памяти), а в правой стороне операции присваивания – r-value (которое производит какое-то значение).

  • Стандарт языка программирования Си++. Где его найти?
  • Как сделать массив объектов, у которых конструктор имеет аргументы
  • Борьба с ошибкой линковки "In function . undefined reference to" при сборке с заранее скомпиленными библиотеками
  • Создание объектов в цикле
  • Про dynamic_cast в сравнении с Pascal
  • Проверка кода: какие статические анализаторы существуют?
  • Про нужность виртуального деструктора
  • Как срабатывают конструкторы, деструкторы и инициализация переменных в C++
  • Многомерные динамические массивы
  • Как работать с указателями на объект
  • Синтаксис объявления указателей на функции в С++ - как построить тип указателя на функцию
  • Указатели на функцию в языке Си/Си++
  • Как получить адрес функции в языке C++
  • Массивы указателей на функцию в языке Си/Си++
  • Указатель в языке C++, хорошее объяснение
  • Как сделать FastCGI сервер на C/C++
  • Концепция объектно-ориентированной парадигмы предельно проста.
  • Небольшой логгер стека вызовов для C++
  • Руководство новичка по эксплуатации компоновщика в C/C++
  • Как препроцессор узнает полный путь к заголовочному файлу библиотеки в GCC
  • Лямбда-функции в C++ (стандарт С++11)
  • Лямбда-выражения в C++0x (С++11)
  • Синтаксис лямбда выражения с языке C++
  • Что обозначает декларация const в описании метода
  • C++: пример использования const-указателя
  • C++: учебные материалы по контейнерным классам и алгоритмам
  • Интервью с Бьерном Страуструпом о языке C++
  • C++: Ключевое слово explicit. Явные конструкторы
  • Модификаторы public, private и protected в C++
  • Права доступа при наследовании в C++
  • Конструктор копирования в C++: объяснение и пример использования
  • Константные методы и константные указатели в C++, краткие примеры
  • Книга "Введение в язык Си++", третье издание
  • Дружественные функции (методы) в С++
  • Дружественные классы в С++
  • Еще одна попытка объяснить, что такое ссылка и указатель, и чем они отличаются
  • Что такое explicitly shared объекты и implicitly shared и чем они отличаются (краткое объяснение)
  • Приведение типов в C++. Терминология, динамический полиморфизм
  • Приведение типов в C++
  • Указатели, ссылки и массивы в C и C++: точки над i tutorial (подробное и простое объяснение)
  • Отличие ссылок от указателей в языке C++
  • Как в C++ различать "константный указатель" и "указатель на константу"
  • Подходы к разработке встраиваемого ПО на C++: Шаблоны
  • Пример перебора std::unordered_map на C++
  • Пример передачи #define определений через флаги компилятора в GCC
  • Что такое lvalue и rvalue в языке C++
  • Что такое l-value и r-value в С++. Простое и короткое объяснение
  • Почему строковый литерал в C++ является lvalue?
  • Значения Lvalue и Rvalue
  • Вопросы и ответы на RSDN. Что это такое lvalue и rvalue?
  • Понимание преинкремента и постинкремента в языке C++
  • Приоритет операций в языке C++
  • Онлайн компиляторы C++
  • Как в C++ можно держать основные настройки программы и прочие конфигурирующие данные не в глобальной области видимости
  • Порядок инициализации в конструкторах
  • Как научиться понимать синтаксис языка C++
  • Возврат значений по ссылке, по адресу (указателю) и по значению в C++
  • Понимание наследования и механизма виртуальных методов в C++
  • Особенности модификатора Const в C++ при работе с указателями и ссылками
  • Глава книги "C++. Практика многопоточного программирования" - Разработка конкурентного кода
  • Как сделать константное свойство в классе в языке C++
  • Примеры работы с байтами через union и через memcpy
  • Особенности языка C и C-style кода
  • Как скомпилировать C++ программу с минимальным размером бинарника в GCC
  • Понимание синтаксиса объявления указателей и массивов
  • Техника поиска кода, использующего конструктор копирования или оператор копирования в языке Си++
  • Рабочий пример простого парсера математических выражений
  • Еще один пример простого парсера математических выражений, с поддержкой именованных констант
  • Как узнать, какие директории будут по-умолчанию включены в INCLUDE path при компиляции C/C++ программ в GCC
  • Кратко: что делают модификаторы override и final
  • Переменные в единицах трансляции: размещение объектов в памяти и время жизни объектов
  • Создание шаблонной фабрики объектов в языке C++
  • Как бороться с ошибкой компиляции undefined reference to `std::cout'
  • Как получить тип переменной в языке C++ в виде строки
  • Размеры базовых математических типов int, long в различных ОС в зависимости от битности
  • Пример использования constexpr и operator для задания величин с размерностью

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

На разных ресурсах нашел разные определения rvalue , lvalue . Как же правильно? right value или read value ? left value или locator value ?

Отслеживать
28.5k 12 12 золотых знаков 58 58 серебряных знаков 118 118 бронзовых знаков
задан 2 авг 2018 в 10:03
2,904 19 19 серебряных знаков 44 44 бронзовых знака
Правильно left и right
2 авг 2018 в 10:22
Никак не правильно, это не расшифровывается. Но произошло от right и left.
2 авг 2018 в 10:26

На мой взгляд, правильный первый вариант, так как взять к примеру простое выражение: a = b + c ,где (a = lvalue), a (b + c = rvalue). Судя по примеру lvalue подразумевает как бы left, но никак не read, тоже самое можно сказать и про rvalue.

2 авг 2018 в 11:22

О чем вопрос вообще? Почему в заголовке речь идет о "rvalue and lvalue references", а в теле вопроса ни слова про references нет и идет речь просто о rvalue и lvalue? В качестве "принятого" выбран ответ, который к "rvalue and lvalue references" вообще не имеет никакого отношения. Что за бессмысленная каша?

2 авг 2018 в 14:32

@Dima Khodan: Нет. Основным критерием качества вопросов (и ответов) на SO являются их полезность для последующих читателей. Если заголовок вопроса никак не соответствует его содержанию, то ценность вопроса - "ниже плинтуса" и он подлежит правке или удалению. Ваше "тут все прекрасно поняли суть" никакого значения не имеет. Я еще раз повторю: почему в заголовке речь идет о "references"? Если ваш вопрос не имеет никакого отношения к "references", то - исправляйте заголовок.

2 авг 2018 в 21:39

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

Исходно речь шла про правую и левую части относительно оператора присваивания. Но правильного варианта такой простой расшифровки нет и быть не может. Термины так и останутся rvalue и lvalue . А вот какой в них вложен смысл чётко прописано в стандарте. Всё это образует категории выражений.

иерархия выражений

  • glvalue (“generalized” lvalue) Выражение, чьё вычисление определяет сущность объекта, битового поля или функции.
  • prvalue (“pure” rvalue) Выражение, чьё вычисление инициализирует объект, битовое поле или вычисляет значение операнда оператора, с соответствии с контекстом использования. Например, литералы 42 , true , nullptr за исключением строковых литералов, которые являются lvalue выражениями.
  • xvalue (“eXpiring” lvalue) Это glvalue, которое обозначает объект или битовое поле, чьи ресурсы могут быть повторно использованы (обычно потому, что они находятся около конца своего времени жизни). Например, результат вызова std::move даёт выражение xvalue.
  • lvalue Это glvalue, которое не является xvalue. Например, имя переменной, функции или члена-данных, независимо от их типа, даже переменная, имеющая тип rvalue-ссылки, образует выражение lvalue.
  • rvalue Это prvalue или xvalue.

Таким образом любое выражение есть в первую очередь lvalue, xvalue или prvalue. rvalue - это уже обобщение.

Отслеживать
ответ дан 2 авг 2018 в 10:27
28.5k 12 12 золотых знаков 58 58 серебряных знаков 118 118 бронзовых знаков
Только вопрос такой. Что значит неполный тип?
2 авг 2018 в 12:04

@DimaKhodan A class that has been declared but not defined, an enumeration type in certain contexts (9.6), or an array of unknown bound or of incomplete element type, is an incompletely-defined object type. Incompletely-defined object types and cv void are incomplete types (6.7.1). Objects shall not be defined to have an incomplete type.

2 авг 2018 в 12:48

Не знаю, что Вы имеете ввиду под вопросом "как правильно". rvalue и lvalue - это категории выражений. Вот что написано в стандарте:

— A glvalue is an expression whose evaluation determines the identity of an object, bit-field, or function.
— A prvalue is an expression > whose evaluation initializes an object or a bit-field, or computes the value of the operand of an operator, as specified by the context in which it appears.
— An xvalue is a glvalue that denotes an object or bit-field whose resources can be reused (usually because it is near the end of its lifetime).
— An lvalue is a glvalue that is not an xvalue.
— An rvalue is a prvalue or an xvalue.

//.
[Note: Historically, lvalues and rvalues were so-called because they could appear on the left- and right-hand side of an assignment (although this is no longer generally true); glvalues are “generalized” lvalues, prvalues are “pure” rvalues, and xvalues are “eXpiring” lvalues. Despite their names, these terms classify expressions, not values. — end note]

Семантика перемещения

В С++ используемые значения мы можем разделить на две группы: lvalue и rvalue . lvalue представляет именованное значение, например, переменные, параметры, константы. С lvalue ассоциирован некоторый адрес в памяти, в котором на постоянной основе хранится некоторое значение. И мы можем lvalue присвоить некоторое значение. А rvalue - это то, что можно только присваивать, например, литералы или результаты выражений. Например:

int n = 5;

Здесь n представляет lvalue, а число 5 - rvalue. Подобные названия приняты, потому что n расположен слева от оператора присваивания (left value), а присваиваемое значение - число 5 справа от оператора присвоения (right value). Другой пример:

int n int k;

Здесь n и k - lvalue, а 5 и выражение n + 7 - rvalue.

rvalue-ссылка

rvalue-ссылка может ссылаться на результат выражения, даже если этот результат представляет временное значение. Привязка к rvalue-ссылке продлевает время жизни такого временного значения. Его память не будет удалена, пока rvalue-ссылка находится в области видимости.

Для установки ссылки rvalue применяются два амперсанда после имени типа:

#include int main() < int n ; int&& tempRef ; // ссылка rvalue std::cout 

В данном случае результат выражения n+3 сохраняется в памяти (в стеке), а ссылка tempRef будет представлять ссылку на это временное значение. Когда завершится функция main, соответственно завершится и область видимости переменной tempRef и будет удалено временное значение, на которое эта переменная ссылается. Стоит отметить, что tempRef, хоть и хранит ссылку на rvalue, само по себе также является lvalue.

Функция std::move

Стоит отметить, что мы не можем установить ссылку rvalue на значение lvalue, например:

int n ; int&& tempRef = n; // ! Так нельзя

Здесь n - lvalue, а ссылке rvalue мы можем только передать значение rvalue. Тем не менее в некоторых ситуациях может возникать необходимость преобразовать lvalue в rvalue. Для этого применяется встроенная функция std::move() , которая имеется по умолчанию в стандартной библиотеке C++:

#include int main() < int n ; int&& tempRef = std::move(n); // преобразуем int в int&& std::cout 

Здесь значение переменной n преобразуется из типа int в тип int&& - rvalue-ссылку на int. В данном случае практического смысла в подобном преобразовании мало, но далее на примере конструктора перемещения мы посмотрим реальную пользу подобной функции.

Также стоит обратить внимание, что при преобразовании константного значения результатом является константная ссылка:

const int m; const int&& mRef = std::move(m); // результат - константная ссылка

rvalue-ссылка как параметр функции

rvalue-ссылка может выступать в качестве параметра функции.

#include void print(std::string&& text) < std::cout int main()

Чтобы указать, что параметр представляет rvalue-ссылку, после типа указываются два амперсанда &&. То есть здесь функция print принимает rvalue-ссылку на значение std::string. При вызове этой функции ей можно передать rvalue:

print("hello");

Но нельзя передать lvalue, поэтому следующие строки НЕ скомпилируются:

std::string message = "hi world"; print(message); // ! Ошибка - передаем lvalue

Однако мы могли бы применить опять же функцию std::move() и преобразовать переменную в rvalue:

print(std::move(message));

Возвращение rvalue из функции

При возвращении значения локальной переменной или параметра компилятор рассматривает значение как rvalue. Но если возвращаемое значение представляет переменную компилятор также может выполнять оптимизацию NRVO (named return value optimization):

#include void print(std::string&& text) < std::cout std::string defaultMessage() < std::string message; return message; > int main() < print(defaultMessage()); // передаем rvalue >

Здесь функция defaultMessage возвращает rvalue, соответственно результат этой функции мы можем передать внутрь функции print. Применяемая оптимизация NRVO означает, что компилятор сохраняет объект результата непосредственно в памяти, предназначенной для хранения возвращаемого функцией значения. То есть после применения NRVO больше не выделяется память для отдельной автоматической переменной с именем message. То есть при выполнении этой программы создается только один объект std::string .

Аналогично происходит при сохранении во внешнюю переменную:

#include std::string defaultMessage() < std::string message; return message; > int main()

Здесь также создается только один объект std::string.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *