Категории выражений



Итак, открываем серию постов, посвященную внутренним сущностям компилятора C++. Их понимания будет достаточно, чтобы существенно повысить производительность ваших программ.



Наверняка в начале изучения языка вам приходилось сталкиваться с фундаментальными понятиями, такими как присвоение значения чему-либо:

  

int a, b;



a = 3; // Корректно

b = a; // Корректно

3 = b; // Ошибка




Исходя из этого простого примера можно сделать вывод, что нельзя просто так взять и присвоить 3 какое-то новое значение. Хотя, казалось бы, это должно быть очень веселым занятием 😊 Напрашивается вопрос, можно ли как-то классифицировать выражения по действиям над ними? Существуют ли еще какие-то особые правила?



Такая классификация действительно возможна и она называется категориями выражений. Итак, встречайте:



lvalue

Так называются те выражения, которыМ задают значение. Они должны быть модифицируемые. Зачастую они располагаются слева от знака равенства, поэтому и получили такое название left-hand value.

  

lvalue

a = 3;




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



rvalue

К этой категории относятся выражения, которыЕ задают значения. Обычно они расположены справа от знака равенства - отсюда название right-hand value.

        rvalue

a = b;




С представителями этой категории необходимо работать независимо от возможности доступа к стеку или куче; иметь возможность читать эти значения, без права изменять их как-либо.



prvalue

К этой категории относятся выражения, которые только задают значения. К такой категории относятся constexpr, литералы и т.д. Например:

        prvalue

a = 3;




Они являются подмножеством rvalue, и в дальнейшем мы не будем делать на этом акцент.



xvalue

К этой категории относятся временные выражения, которые будут в скором времени уничтожены (eXpiring value). В некоторых случаях, их ресурсы могут быть эффективно переиспользованы. Пока оставлю вас без примера 😉





Думаю, не будет ошибкой заявить, что отношение к какой-либо категории определяет набор операций, которые могут быть выполнены над данными. С помощью этой модели компилятор проверяет, не совершил ли пользователь логическую ошибку и может принимать решения об оптимизации (исходя из ограничений).



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



Мне так же следует сделать акцент на том, что категория выражений определяется окружающим кодом. Это означает, что в контексте текущей строчки кода категория выражений выводится исходя из предшествующего кода и операций, которые разработчик приказал выполнить. На следующей строчке эта категория может быть преобразована из исходной в другую.



Так, например, мы знаем, что нет никаких ограничений, чтобы скопировать переменную a в b. Значит, переменная a может быть преобразована к rvalue :

  

lvalue rvalue

a = 3;



lvalue lvalue -> rvalue

b = a;




Действительно, lvalue может быть неявно приведено к rvalue, но не наоборот! Так, например, численная константа 3 независимо от контекста всегда будет rvalue, т.к. её значение нельзя поменять ни при каких обстоятельствах. Если это правило нарушается, компилятор вполне заслуженно бьет по рукам.



Рассмотрим другой пример:

rvalue     rvalue 

(a + b) = a // Ошибка!




Хоть сумма a + b и может быть образована из двух lvalue, но оператор + возвращает rvalue. Результат сложения должен быть присвоен другой переменной или использован для других операций. По сути, он не был сохранен в переменную на стек или кучу из области видимости, поэтому как ему можно присвоить хоть какое-то иное значение?



Продолжение в комментариях!



#cppcore #memory #algorithm