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
2. A referência universal && no modelo
(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?
- 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;
- 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】
- 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;
- Se você não deseja que rr1 seja modificado, você pode usar const int&& rr1 para fazer referência;
- 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;
- 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;
- 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++, move
um 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】
- Conforme mostrado acima, a função Func() é chamada no modelo de função PerfectForward();
- 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;
- 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】
- No exemplo acima, definimos dois modelos de função sobrecarregados
Func
, um que usa um parâmetro de referência lvalueT& arg
e outro que usa um parâmetro de referência diretaT&& arg;
- Em seguida, definimos uma função de modelo
PerfectForward
cujo parâmetro também é uma referência diretaT&& arg
. DentroPerfectForward
da função,Func
processamos os parâmetros passados chamando a função; - Através do mecanismo de sobrecarga de função, o parâmetro lvalue passado será correspondido à
Func
função que recebe a referência lvalue, e o parâmetro rvalue passado será correspondido àFunc
função que recebe a referência direta, de modo a distinguir e processar corretamente; - 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】
- O && no template não representa uma referência de rvalue, mas sim uma referência universal, que pode receber tanto lvalues quanto rvalues.
- A referência universal do modelo fornece apenas a capacidade de receber referências lvalue e referências rvalue.
- No entanto, a única função dos tipos de referência é limitar os tipos recebidos, e eles degeneram em lvalues em usos subsequentes.
- 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.
- O encaminhamento perfeito é frequentemente usado com referências de encaminhamento e a função std::forward;
- 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; - 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! ! !