В С++ используемые значения мы можем разделить на две группы: lvalue и rvalue. lvalue представляет именованное значение, например, переменные, параметры, константы. С lvalue ассоциирован некоторый адрес в памяти, в котором на постоянной основе хранится некоторое значение. И мы можем lvalue присвоить некоторое значение. А rvalue - это то, что можно только присваивать, например, литералы или результаты выражений. Например:
int n = 5;
Здесь n представляет lvalue, а число 5 - rvalue. Подобные названия приняты, потому что n расположен слева от оператора присваивания (left value), а присваиваемое значение - число 5 справа от оператора присвоения (right value). Другой пример:
int n{5} int k{n + 7};
Здесь n и k - lvalue, а 5 и выражение n + 7
- rvalue.
rvalue-ссылка может ссылаться на результат выражения, даже если этот результат представляет временное значение. Привязка к rvalue-ссылке продлевает время жизни такого временного значения. Его память не будет удалена, пока rvalue-ссылка находится в области видимости.
Для установки ссылки rvalue применяются два амперсанда после имени типа:
#include <iostream> int main() { int n {5}; int&& tempRef {n+3}; // ссылка rvalue std::cout << tempRef << std::endl; // 8 }
В данном случае результат выражения n+3
сохраняется в памяти (в стеке), а ссылка tempRef будет представлять ссылку на это временное значение. Когда завершится функция main, соответственно завершится и область видимости
переменной tempRef и будет удалено временное значение, на которое эта переменная ссылается. Стоит отметить, что tempRef, хоть и хранит ссылку на rvalue, само по себе также является
lvalue.
Стоит отметить, что мы не можем установить ссылку rvalue на значение lvalue, например:
int n {5}; int&& tempRef = n; // ! Так нельзя
Здесь n - lvalue, а ссылке rvalue мы можем только передать значение rvalue. Тем не менее в некоторых ситуациях может возникать необходимость преобразовать lvalue в rvalue. Для этого применяется встроенная функция std::move(), которая имеется по умолчанию в стандартной библиотеке C++:
#include <iostream> int main() { int n {5}; int&& tempRef = std::move(n); // преобразуем int в int&& std::cout << tempRef << std::endl; // 5 }
Здесь значение переменной n преобразуется из типа int в тип int&& - rvalue-ссылку на int. В данном случае практического смысла в подобном преобразовании мало, но далее на примере конструктора перемещения мы посмотрим реальную пользу подобной функции.
Также стоит обратить внимание, что при преобразовании константного значения результатом является константная ссылка:
const int m{2}; const int&& mRef = std::move(m); // результат - константная ссылка
rvalue-ссылка может выступать в качестве параметра функции.
#include <iostream> void print(std::string&& text) { std::cout << text << std::endl; } int main() { print("hello"); }
Чтобы указать, что параметр представляет 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. Но если возвращаемое значение представляет переменную компилятор также может выполнять оптимизацию NRVO (named return value optimization):
#include <iostream> void print(std::string&& text) { std::cout << text << std::endl; } std::string defaultMessage() { std::string message{"hello world"}; return message; } int main() { print(defaultMessage()); // передаем rvalue }
Здесь функция defaultMessage возвращает rvalue, соответственно результат этой функции мы можем передать внутрь функции print. Применяемая оптимизация NRVO означает, что компилятор сохраняет объект результата непосредственно в памяти, предназначенной для хранения возвращаемого функцией значения. То есть после применения NRVO больше не выделяется память для отдельной автоматической переменной с именем message. То есть при выполнении этой программы создается только один объект std::string.
Аналогично происходит при сохранении во внешнюю переменную:
#include <iostream> std::string defaultMessage() { std::string message{"hello world"}; return message; } int main() { std::string text = defaultMessage(); std::cout << text << std::endl; }
Здесь также создается только один объект std::string.
Эта была вводная информация про значения rvalue и как с ними работать. В следующих статьях посмотрим, где в применяется в практическом плане.