[C++] —— Novos recursos do C++ 11 "referência de valor e semântica de movimentação"

Prefácio:

  • Nesta edição, apresentaremos conhecimentos relevantes sobre referências de rvalue em C++. Para o conhecimento do conteúdo desta questão, todos devem ser capazes de dominá-la, sendo ela um objeto fundamental de investigação na entrevista.

Índice

(1) referência lvalue e referência rvalue

1. O que é um valor? O que é uma referência lvalue?

2. O que é um valor? O que é uma referência de valor?

(2) Comparação entre referência lvalue e referência rvalue

(3) Cenários de uso e importância das referências de valor

(4) Encaminhamento perfeito 

1. Conceito

2. A referência universal && no modelo

3、std::forward

Resumir


(1) referência lvalue e referência rvalue

Há uma sintaxe de referência na sintaxe C++ tradicional e o novo recurso de sintaxe de referência rvalue no C++ 11, portanto, de agora em diante, a referência que aprendemos antes é chamada de referência lvalue. Independentemente de ser uma referência lvalue ou uma referência rvalue, um alias é fornecido ao objeto.

1. O que é um valor? O que é uma referência lvalue?

Existem referências no padrão C++98/03, que são representadas por "&" . No entanto, este método de referência tem uma falha, ou seja, em circunstâncias normais, apenas lvalues ​​​​em C++ podem ser operados e referências não podem ser adicionadas a rvalues . por exemplo:
 

int main()
{
	int num = 10;
	int& b = num; //正确
	int& c = 10; //错误

	return 0;
}

Exibição de saída:

 【explicar】

  • Conforme mostrado acima, o compilador nos permite criar uma referência ao num lvalue, mas não ao 10 rvalue. Portanto, as referências no padrão C++98/03 também são chamadas de referências lvalue.
     

Então, o que exatamente é um lvalue? O que é uma referência lvalue?

  1. Um lvalue é uma expressão que representa dados (como um nome de variável ou um ponteiro desreferenciado), podemos obter seu endereço + atribuir um valor , um lvalue pode aparecer no lado esquerdo de um símbolo de atribuição e um rvalue não pode aparecer em o lado esquerdo de um símbolo de atribuição;
  2. O lvalue após o modificador const, quando definido, não pode receber um valor, mas seu endereço pode ser obtido. Uma referência lvalue é uma referência a um lvalue e um alias é fornecido ao lvalue.
     

Nota : Embora o padrão C++98/03 não suporte o estabelecimento de referências de lvalue não const para rvalues, ele permite o uso de referências de lvalue const para operar em rvalues. Ou seja, uma referência lvalue constante pode operar tanto em lvalues ​​quanto em rvalues, por exemplo:
 

int main()
{
    // 以下的p、b、c、*p都是左值
    int* p = new int(0);
    int b = 1;
    const int c = 2;

    // 以下几个是对上面左值的左值引用
    int*& rp = p;
    int& rb = b;

    //左值引用给右值取别名
    const int& rc = c;

    int& pvalue = *p;

    return 0;
}

2. O que é um valor? O que é uma referência de valor?
 

Sabemos que rvalues ​​muitas vezes não possuem nomes , portanto só podem ser usados ​​por referência. Isso cria um problema. No desenvolvimento real, podemos precisar modificar o rvalue (necessário ao implementar a semântica de movimento). Obviamente, a forma de referência de lvalue não é viável.


Por esse motivo, o padrão C++ 11 introduz outro método de referência, denominado referência de rvalue, representado por "&&":

  • Um rvalue também é uma expressão que representa data , como: constante literal, valor de retorno de expressão, valor de retorno de função (este não pode ser um retorno de referência de lvalue), etc.;
  • Um rvalue pode aparecer no lado direito de um símbolo de atribuição, mas não pode aparecer no lado esquerdo de um símbolo de atribuição.Um rvalue não pode receber um endereço ;
  • Uma referência de rvalue é uma referência a um rvalue, fornecendo um alias ao rvalue.

int main()
{
	double x = 1.1, y = 2.2;

	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);

	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);

	return 0;
}

Exibição de saída:

 Mas se forem as seguintes expressões, ocorrerá um erro:

10 = 1;
x + y = 1;
fmin(x, y) = 1;

A saída mostra:

Deve-se observar que o endereço do rvalue não pode ser obtido , mas após o alias ser fornecido ao rvalue, isso fará com que o rvalue seja armazenado em um local específico, e o
endereço desse local poderá ser obtido. Por exemplo, o código a seguir mostra:

int main()
{
	double x = 1.1, y = 2.2;

	int&& rr1 = 10;
	double&& rr2 = x + y;
	cout << rr1 << " " << rr2 << " " << endl;

	rr1 = 20;
	rr2 = 5.5;
	cout << rr1 << " " << rr2 << " " << endl;

	return 0;
}

Exibição de saída:

 Quando não queremos ser modificados, podemos adicionar a palavra-chave [const]:

【explicar】

  1. O endereço do literal 10 não pode ser obtido, mas após rr1 ser referenciado, o endereço de rr1 pode ser obtido ou rr1 pode ser modificado;
  2. Se você não deseja que rr1 seja modificado, você pode usar const int&& rr1 para fazer referência;
  3. Não é incrível? Compreender os cenários reais de uso de referências de rvalue não reside nisso, e esse recurso não é importante.
     

(2) Comparação entre referência lvalue e referência rvalue

Resumo de referência de valor:

  • 1. Uma referência lvalue só pode fazer referência a lvalues, não a rvalues.
  • 2. No entanto, uma referência const lvalue pode referir-se a um lvalue e a um rvalue.

int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a; // ra为a的别名
    
    return 0;
}

Exibição de saída:

 Outro exemplo é o seguinte:

int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra2 = 10; // 编译失败,因为10是右值

    return 0;
}

Exibição de saída:

 As referências de Lvalue só podem se referir a lvalues, não a rvalues. Mas quando adicionamos const, a referência lvalue pode ser o alias de rvalue:

int main()
{
    int a = 10;
    // const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
    
    return 0;
}

Exibição de saída:

【explicar】

Vale a pena mencionar que embora a sintaxe C++ suporte a definição de referências de rvalue constantes, tais referências de rvalue definidas não têm uso prático:

const int& ra3 = 10;
  1. Por um lado, as referências de rvalue são usadas principalmente para semântica de movimentação e encaminhamento perfeito , onde a primeira requer permissão para modificar rvalues;
  2. Em segundo lugar, a função de uma referência de valor constante é referir-se a um valor não modificável.Este trabalho pode ser completado por uma referência de valor constante.
     


Resumo de referência de Rvalue:

  • 1. As referências de Rvalue só podem referir-se a rvalues, não a lvalues.
  • 2. Mas as referências de rvalue podem mover lvalues ​​subsequentes.
     

Exibição de código:

 Ocorrerá um erro ao fazer referência a um lvalue:


A conversão de lvalues ​​em referências de rvalue pode ser suportada por meio de movimentação 

 Em C++, moveum modelo de função que converte um determinado objeto na referência de rvalue correspondente. Ele não executa a operação real de movimentação de memória, mas marca o objeto como um rvalue que pode ser movido. Dessa forma, os usuários podem aproveitar essa marcação para obter uma semântica de movimentação mais eficiente.


(3) Cenários de uso e importância das referências de valor

Podemos ver anteriormente que as referências lvalue podem referenciar lvalues ​​​​e rvalues, então por que o C++ 11 também propõe referências rvalue? É supérfluo? Vamos dar uma olhada nas deficiências das referências lvalue e como as referências rvalue podem compensar essa deficiência!

Existe o seguinte código: 

 【explicar】

Primeiro, para res1 e res2 no código acima, eles são lvalue e rvalue respectivamente;

A seguir, vamos pensar sobre isso, há alguma diferença entre copiar lvalues ​​​​e rvalues?

  • Se for um tipo embutido, a diferença entre eles não é muito grande, mas para o tipo customizado, a diferença é muito grande.
  • Por causa do rvalue do tipo personalizado, ele geralmente é chamado de valor final em muitos lugares. Geralmente é o valor de retorno de algumas expressões, uma chamada de função, etc.;
  • Para rvalues, ele é dividido em pré-valores (em geral, tipos integrados) e valores de vontade (em geral, tipos personalizados)

Para o res1 acima, como um lvalue, não podemos operar nele e só podemos fazer uma cópia profunda. Porque embora pareça uma tarefa aqui, na verdade deveria ser uma construção de cópia;

Quanto ao res2, ele próprio é um rvalue. Se for um tipo personalizado como um valor moribundo, não precisamos copiá-lo. Neste ponto, é introduzido o conceito de implementação de referência de valor que desencadeia a construção.


Por exemplo, agora existe uma string que simulamos escrita à mão:

namespace zp
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;

			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str);
			swap(tmp);
		}


		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;

				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		string operator+(char ch)
		{
			string tmp(*this);
			tmp += ch;
			return tmp;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

Quando observamos res1 e res2 acima neste cenário:

 Podemos descobrir que o rvalue aqui é uma cópia profunda correspondente, o que obviamente causa desperdício desnecessário. Para resolver os problemas acima, podemos introduzir o conceito de “construção em movimento”:

// 移动构造
string(string&& s)
	:_str(nullptr)
{
	cout << "string(string&& s) -- 移动拷贝" << endl;
	swap(s);
}

Imediatamente depois, executando novamente o código acima, podemos descobrir que o compilador reconhecerá automaticamente:

Neste ponto, o que podemos fazer quando queremos apenas converter s1 em um rvalue? Na verdade, é muito simples ( o movimento é refletido aqui ):

 Exibição de saída:

Também podemos descobrir através da depuração que o efeito esperado é realmente alcançado neste momento:

 【resumo】

Do exposto, podemos descobrir que o benefício das referências lvalue é reduzir diretamente a cópia

Os cenários de uso de referências lvalue podem ser divididos nas duas partes a seguir:

  • Tanto os parâmetros quanto os valores de retorno podem melhorar a eficiência

Deficiências das referências lvalue:

  • Mas quando o objeto retornado pela função é uma variável local, ele não existe fora do escopo da função, portanto não pode ser retornado por referência lvalue, mas só pode ser retornado por valor.

Por exemplo, agora temos o seguinte código: 

    zp::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		zp::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;

			str += ('0' + x);
		}

		if (flag == false)
		{
			str += '-';
		}

		std::reverse(str.begin(), str.end());
		return str;
	}

【ilustrar】

  • Como você pode ver na função zp ::string to_string(int value), apenas o retorno por valor pode ser usado aqui, e o retorno por valor causará pelo menos uma construção de cópia (se forem alguns compiladores mais antigos, podem ser duas cópias construções ) .

 A seguir, vamos imprimir para ver qual é o resultado:

 Neste ponto, o custo da devolução por valor foi bastante resolvido:

 As referências Rvalue e a semântica de movimento resolvem os problemas acima:

  • Adicione uma estrutura de movimentação a zp::string. A essência da estrutura de movimentação é roubar os recursos do rvalue do parâmetro. Se o espaço já estiver lá, não há necessidade de fazer uma cópia profunda, por isso é chamado uma estrutura de movimento, que consiste em roubar recursos de outras pessoas para se construir.

Em seguida, execute as duas chamadas de zp::to_string acima, descobriremos que a construção de cópia de cópia profunda não é chamada aqui, mas a construção de movimento é chamada. Não há novo espaço na construção de movimento para copiar dados, então a eficiência é melhorado.


C++ 11 não apenas possui construção de movimentação, mas também atribuição de movimentação:

Adicione uma função de atribuição de movimentação na classe zp::string e, em seguida, chame zp::to_string(1234), mas desta vez o objeto rvalue retornado por zp::to_string(1234) é atribuído ao objeto ret1 e a movimentação é chamada neste momento estrutura.

Exibição de saída:

 【explicar】

  • Depois de executar aqui, vemos que uma construção de movimento e uma atribuição de movimento são chamadas. Porque se um objeto existente for usado para recebê-lo, o compilador não poderá otimizá-lo. Na função zp::to_string, um objeto temporário será gerado primeiro com a construção de geração de str, mas podemos ver que o compilador é muito inteligente aqui para reconhecer str como um rvalue e chamar a construção de movimento. Em seguida, use esse objeto temporário como o valor de retorno da chamada de função zp::to_string e atribua-o a ret1. A atribuição de movimentação chamada aqui.

(4) Encaminhamento perfeito 

1. Conceito

  • O encaminhamento perfeito é um recurso introduzido no C++ 11, com o objetivo de alcançar a capacidade de passar com precisão tipos de parâmetros em modelos de função;
  • É usado principalmente para preservar a categoria de valor do parâmetro real passado para o modelo de função e encaminhá-lo para a função chamada internamente, conseguindo assim a preservação total do tipo e da categoria de valor;

por exemplo:

template<typename T>
void PerfectForward(T t)
{
    Fun(t);
}

【explicar】

  1. Conforme mostrado acima, a função Func() é chamada no modelo de função PerfectForward();
  2. Com base nisso, encaminhamento perfeito refere-se a: se o parâmetro t recebido pela função PerfectForward() é um lvalue, então o parâmetro t passado para Func() por esta função também é um lvalue;
  3. Por outro lado, se o parâmetro t recebido pela função function() for um rvalue, então o parâmetro t passado para a função Func() também deverá ser um rvalue.

Utilizando qualquer forma de citação, o encaminhamento pode ser conseguido, mas a perfeição não é garantida. Portanto, se usarmos a linguagem C++ sob o padrão C++98/03, podemos usar a sobrecarga do modelo de função para obter um encaminhamento perfeito, por exemplo:

template<typename T>
void Func(T& arg)
{
    cout << "左值引用:" << arg << endl;
}

template<typename T>
void Func(T&& arg)
{
    cout << "右值引用:" << arg << endl;
}

template<typename T>
void PerfectForward(T&& arg)
{
    Func(arg);  // 利用重载的process函数进行处理
}

int main()
{
    int value = 42;
    PerfectForward(value);       // 传递左值
    PerfectForward(123);         // 传递右值

    return 0;
}

Exibição de saída:

 【explicar】

  1. No exemplo acima, definimos dois modelos de função sobrecarregados  Func, um que usa um parâmetro de referência lvalue T& arge outro que usa um parâmetro de referência diretaT&& arg;
  2. Em seguida, definimos uma função de modelo PerfectForwardcujo parâmetro também é uma referência direta T&& arg. Dentro PerfectForwardda função, Funcprocessamos os parâmetros passados ​​chamando a função;
  3. Através do mecanismo de sobrecarga de função, o parâmetro lvalue passado será correspondido à Funcfunção que recebe a referência lvalue, e o parâmetro rvalue passado será correspondido à Funcfunção que recebe a referência direta, de modo a distinguir e processar corretamente;
  4. Através da sobrecarga de modelos de função, podemos distinguir lvalues ​​​​e rvalues ​​​​de acordo com os tipos de parâmetros e tratá-los separadamente, realizando correspondências e operações precisas para diferentes categorias de valores.

2. A referência universal && no modelo

Obviamente, o uso acima mencionado de funções de modelo sobrecarregadas para obter um encaminhamento perfeito também tem desvantagens. Este método de implementação só é aplicável ao caso em que a função de modelo possui apenas um pequeno número de parâmetros. Caso contrário, é necessário escrever um grande número de modelos de função sobrecarregados, resultando em redundância de código. Para facilitar aos usuários a obtenção de um encaminhamento perfeito mais rapidamente, o padrão C++ 11 permite o uso de referências de rvalue em modelos de função para obter um encaminhamento perfeito.

Tomemos a função PerfectForward() como exemplo. Para obter um encaminhamento perfeito no padrão C++ 11, você só precisa escrever a seguinte função de modelo:

//模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
template<typename T>
void PerfectForward(T&& t)
{
    Fun(t);
}

Tome o seguinte código como exemplo:

void Fun(int& x) 
{ 
	cout << "左值引用" << endl;
}
void Fun(const int& x)
{
	cout << "const 左值引用" << endl; 
}
void Fun(int&& x)
{
	cout << "右值引用" << endl; 
}
void Fun(const int&& x) 
{
	cout << "const 右值引用" << endl; 
}

template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	PerfectForward(10); // 右值

	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值

	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

Exibição de saída:

 【explicar】

  1. O && no template não representa uma referência de rvalue, mas sim uma referência universal, que pode receber tanto lvalues ​​quanto rvalues.
  2. A referência universal do modelo fornece apenas a capacidade de receber referências lvalue e referências rvalue.
  3. No entanto, a única função dos tipos de referência é limitar os tipos recebidos, e eles degeneram em lvalues ​​em usos subsequentes.
  4. Se quisermos manter seus atributos lvalue ou rvalue durante o processo de transferência, precisamos usar o encaminhamento perfeito que aprenderemos a seguir.
     

3、std::forward

Os desenvolvedores do padrão C++ 11 já encontraram uma boa solução para nós. O novo padrão também introduz uma função de modelo forword<T>(). Só precisamos chamar essa função para resolver esse problema de maneira muito conveniente.

  1. O encaminhamento perfeito é frequentemente usado com referências de encaminhamento e a função std::forward;
  2. A referência encaminhada é um tipo de referência especial, &&declarado usando sintaxe, usado para capturar os parâmetros reais passados ​​no modelo de função;
  3. std::forward é uma função de modelo usada para encaminhar referências como referências rvalue ou lvalue dentro de um modelo de função.

O uso deste modelo de função é demonstrado da seguinte forma:

void Fun(int& x) 
{ 
	cout << "左值引用" << endl;
}
void Fun(const int& x)
{
	cout << "const 左值引用" << endl; 
}
void Fun(int&& x)
{
	cout << "右值引用" << endl; 
}
void Fun(const int&& x) 
{
	cout << "const 右值引用" << endl; 
}

template<typename T>
void PerfectForward(T&& t)
{
	// forward<T>(t)在传参的过程中保持了t的原生类型属性。
	Fun(std::forward<T>(t));
}
int main()
{
	PerfectForward(10); // 右值

	int a;
	PerfectForward(a); // 左值
	PerfectForward(move(a)); // 右值

	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(move(b)); // const 右值
	return 0;
}

O resultado da execução do programa é:

Através do encaminhamento perfeito, podemos manipular corretamente a categoria de valor do parâmetro real passado no modelo de função e encaminhá-lo para a função interna para obter a retenção completa do tipo e categoria de valor e melhorar a flexibilidade e eficiência do código.


Resumir

Depois de aprender isso, alguns leitores podem não conseguir lembrar claramente se as referências lvalue e as referências rvalue podem se referir a lvalues ​​​​ou rvalues. Aqui está uma tabela para todos lembrarem:
 

  •  Na tabela, Y significa suportado e N significa não suportado.

Acima está todo o conhecimento sobre referências lvalue e referências rvalue! Obrigado a todos por assistir e apoiar! ! !

Acho que você gosta

Origin blog.csdn.net/m0_56069910/article/details/132475156
Recomendado
Clasificación