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

rvalue

Последнее обновление: 23.03.2023

В С++ используемые значения мы можем разделить на две группы: 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-ссылка находится в области видимости.

Для установки ссылки 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.

Функция std::move

Стоит отметить, что мы не можем установить ссылку 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-ссылка как параметр функции

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 из функции

При возвращении значения локальной переменной или параметра компилятор рассматривает значение как 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 и как с ними работать. В следующих статьях посмотрим, где в применяется в практическом плане.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850