Índice
2. Categorias de valor e conceitos relacionados
4. referência de valor, referência de valor
5.1. Por que a semântica de movimentação é necessária
5.2. Definição de semântica móvel
5.3, construtor de transferência
5.4. Função de atribuição de transferência
6. Função de biblioteca padrão std::move
7. Encaminhamento perfeito std::forward
Resumo do desenvolvimento de funções comuns do VC++ (lista de artigos de coluna, bem-vindo à assinatura, atualizações contínuas...) https://blog.csdn.net/chenlycly/article/details/124272585 série de tutoriais sobre solução de anomalias de software C++ de entrada para proficiência (lista de artigos da coluna), bem-vindo para se inscrever e continuar atualizando...) https://blog.csdn.net/chenlycly/article/details/125529931 Ferramentas de análise de software C++ desde a entrada até a coleção de casos de domínio (coluna artigo está sendo atualizado...) https://blog.csdn.net/chenlycly/article/details/131405795 Noções básicas e avançadas de C/C++ (artigo da coluna, atualizado continuamente...) https://blog.csdn.net /chenlycly/category_11931267.html Os novos recursos do C++ 11 são muito importantes. Como desenvolvedor C++, é necessário aprendê-los. Eles não apenas serão abordados em exames escritos e entrevistas, mas também serão usados em larga escala em atividades abertas Código fonte. Tomemos como exemplo o projeto WebRTC de código aberto usado por muitos softwares de videoconferência e transmissão ao vivo. O código WebRTC usa extensivamente os novos recursos do C++ 11 e superior. Para entender seu código-fonte, você deve entender esses novos recursos do C++. Portanto, no próximo período, darei a você uma explicação detalhada dos novos recursos do C++ 11 com base na prática de trabalho para referência.
1. Introdução
C++ 11 introduz o conceito de movimento de objetos, que é a capacidade de mover em vez de copiar objetos. Mover objetos pode efetivamente melhorar o desempenho do programa.
Para oferecer suporte a operações de movimentação, o C++ 11 introduz um novo tipo de referência – referências rvalue. A chamada referência de rvalue é uma referência que deve ser vinculada a um rvalue. A referência de rvalue é obtida através do operador &&. Hoje, vamos falar sobre lvalues, referências de lvalue, rvalues e referências de rvalue em detalhes.
2. Categorias de valor e conceitos relacionados
No C++ 11, as categorias de valor são divididas principalmente em lvalues, referências de lvalue, rvalues e referências de rvalue. Uma referência lvalue é um aplicativo vinculado a um lvalue e uma referência rvalue é uma referência vinculada a um rvalue. Quando a referência de valor T&& aparece no parâmetro de uma função de modelo, T é o tipo de modelo e T&& é uma referência universal. As referências universais podem receber parâmetros lvalue, bem como parâmetros rvalue. A dobragem de referência pode então ocorrer quando os tipos de parâmetros de função são deduzidos. As funções da biblioteca padrão std::move e std::forward também estarão envolvidas.
3. Lvalor, rvalor
Na linguagem C, frequentemente mencionamos lvalue (lvalue) e rvalue (rvalue). Um dos métodos de identificação mais típicos é que em uma expressão de atribuição, o que aparece no lado esquerdo do sinal de igual é um “lvalue”, e o que aparece no lado direito do sinal de igual é chamado de “rvalue”. como:
int b = 1;
int c = 2;
int a = a + b;
Nesta expressão de atribuição, a é um lvalue e b + c é um rvalue.
No entanto, há outro ditado amplamente reconhecido em C++, ou seja, aqueles que podem receber endereços e ter nomes são lvalores e, inversamente, aqueles que não podem receber endereços e não têm nomes são rvalores. Então, nesta expressão de atribuição de adição, &a é uma operação permitida, mas operações como &(b + c) não serão compiladas. Portanto, a é um lvalue e (b + c) é um rvalue. Em relação aos lvalues, os rvalues representam constantes literais, expressões, valores de retorno sem referência de funções, etc.
Para resumir aqui, o que é um lvalue:
1) Pode receber um valor (condições suficientes, mas não necessárias), lvalues não necessariamente podem ser atribuídos, como variáveis const, a inteligência atribui um valor inicial durante a inicialização e não pode ser atribuída posteriormente 2) Pode acessar endereços (suficientes,
mas não são condições necessárias), lvalores podem não ser. Pode receber endereços, como a variável de registro na linguagem C: registro int i= 3. C++ 11 cancelou o suporte para registro e será ignorado durante a compilação. Para outro exemplo, variáveis de campo de bits na linguagem C não podem ser endereçadas:
struct St{ int m:3;} St st; st.m = 3; especifica que o membro do tipo int m ocupa 3 bytes.
3) Uma referência lvalue pode ser inicializada (uma condição necessária, mas não suficiente). Um lvalue pode inicializar uma referência lvalue, por exemplo: int m = 3; int& n = m;, e um rvalue não pode inicializar uma referência lvalue, por exemplo : const int& m = 3 ; Como 3 é um rvalue, a referência lvalue não pode ser inicializada, portanto a compilação reportará um erro.
4) Literais são rvalores puros. Por exemplo, números imediatos como 1, 2 e 3 são literais. Observe que a string constante "xyz" é um lvalue e o endereço dessa string pode ser obtido, ou seja, &" xyz".
5) Os recursos no valor final podem ser roubados. O valor de retorno da função é um rvalue puro e retorna uma referência de rvalue, como a função std::move.
4. referência de valor, referência de valor
Uma referência lvalue é um tipo que se refere a um lvalue, e uma referência rvalue é um tipo que se refere a um rvalue. Tanto as referências lvalue quanto as referências rvalue são tipos de referência. Quer uma referência lvalue ou uma referência rvalue seja declarada, ela deve ser inicializada imediatamente. A razão pode ser entendida como o fato de o próprio tipo de referência não possuir a memória do objeto vinculado, mas ser apenas um alias do objeto.
Uma referência lvalue é um alias para o valor de uma variável nomeada, enquanto uma referência rvalue é um alias para uma variável sem nome (anônima).
Exemplo de uma referência lvalue:
int &a = 2; // 左值引用绑定到右值,编译失败, err
int b = 2; // 非常量左值
const int &c = b; // 常量左值引用绑定到非常量左值,编译通过, ok
const int d = 2; // 常量左值
const int &e = c; // 常量左值引用绑定到常量左值,编译通过, ok
const int &b = 2; // 常量左值引用绑定到右值,编程通过, ok
"const type &" é um tipo de referência "universal", que pode aceitar lvalues não constantes, lvalues constantes e rvalues para inicializá-lo;
referência de valor, representada por &&:
int && r1 = 22;
int x = 5;
int y = 8;
int && r2 = x + y;
T && a = ReturnRvalue();
Normalmente, uma referência de rvalue não pode ser vinculada a nenhum lvalue:
int c;
int && d = c; //err
Vejamos um exemplo de teste:
void process_value(int & i) //参数为左值引用
{
cout << "LValue processed: " << i << endl;
}
void process_value(int && i) //参数为右值引用
{
cout << "RValue processed: " << i << endl;
}
int main()
{
int a = 0;
process_value(a); //LValue processed: 0
process_value(1); //RValue processed: 1
return 0;
}
5. Semântica Móvel
5.1. Por que a semântica de movimentação é necessária
As referências Rvalue são usadas para dar suporte à semântica de transferência. A semântica de transferência pode transferir recursos (heap, objetos do sistema, etc.) de um objeto para outro, o que pode reduzir a criação, cópia e destruição desnecessárias de objetos temporários e pode melhorar significativamente o desempenho de aplicativos C++. A manutenção (criação e destruição) de objetos temporários tem um impacto severo no desempenho.
A semântica de transferência é oposta à semântica de cópia e pode ser comparada ao corte e cópia de arquivos.Quando copiamos um arquivo de um diretório para outro, a velocidade é muito mais lenta do que o corte. Através da semântica de transferência, os recursos em objetos temporários podem ser transferidos para outros objetos.
5.2. Definição de semântica móvel
No mecanismo C++ existente, podemos definir construtores de cópia e funções de atribuição. Para implementar a semântica de transferência, você precisa definir um construtor de transferência e também pode definir um operador de atribuição de transferência. Para copiar e atribuir rvalores, o construtor de transferência e o operador de atribuição de transferência são chamados.
Se o construtor de transferência e o operador de cópia de transferência não estiverem definidos, o mecanismo existente será seguido e o construtor de cópia e o operador de atribuição serão chamados. Funções e operadores comuns também podem usar operadores de referência de valor para implementar a semântica de transferência.
5.3, construtor de transferência
Vejamos primeiro um exemplo de construtor de transferência:
class MyString
{
public:
MyString(const char *tmp = "abc")
{//普通构造函数
len = strlen(tmp); //长度
str = new char[len+1]; //堆区申请空间
strcpy(str, tmp); //拷贝内容
cout << "普通构造函数 str = " << str << endl;
}
MyString(const MyString &tmp)
{//拷贝构造函数
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);
cout << "拷贝构造函数 tmp.str = " << tmp.str << endl;
}
//移动构造函数
//参数是非const的右值引用
MyString(MyString && t)
{
str = t.str; //拷贝地址,没有重新申请内存
len = t.len;
//原来指针置空
t.str = NULL;
cout << "移动构造函数" << endl;
}
MyString &operator= (const MyString &tmp)
{//赋值运算符重载函数
if(&tmp == this)
{
return *this;
}
//先释放原来的内存
len = 0;
delete []str;
//重新申请内容
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);
cout << "赋值运算符重载函数 tmp.str = " << tmp.str << endl;
return *this;
}
~MyString()
{//析构函数
cout << "析构函数: ";
if(str != NULL)
{
cout << "已操作delete, str = " << str;
delete []str;
str = NULL;
len = 0;
}
cout << endl;
}
private:
char *str = NULL;
int len = 0;
};
MyString func() //返回普通对象,不是引用
{
MyString obj("mike");
return obj;
}
int main()
{
MyString &&tmp = func(); //右值引用接收
return 0;
}
Semelhante ao construtor de cópia, há alguns pontos a serem observados:
1) O símbolo do parâmetro (rvalue) deve ser um símbolo de referência de rvalue, ou seja, “&&”.
2) O parâmetro (rvalue) não pode ser uma constante, pois precisamos modificar o rvalue.
3) Os links de recursos e tags dos parâmetros (rvalue) devem ser modificados, caso contrário, o destruidor do rvalue liberará os recursos, e os recursos transferidos para o novo objeto serão inválidos.
Com referência de rvalue e semântica de transferência, quando projetamos e implementamos classes, para classes que precisam se aplicar dinamicamente a um grande número de recursos, devemos projetar construtores de transferência e funções de atribuição de transferência para melhorar a eficiência do aplicativo.
5.4. Função de atribuição de transferência
Vejamos diretamente o exemplo da função de atribuição de transferência:
class MyString
{
public:
MyString(const char *tmp = "abc")
{//普通构造函数
len = strlen(tmp); //长度
str = new char[len+1]; //堆区申请空间
strcpy(str, tmp); //拷贝内容
cout << "普通构造函数 str = " << str << endl;
}
MyString(const MyString &tmp)
{//拷贝构造函数
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);
cout << "拷贝构造函数 tmp.str = " << tmp.str << endl;
}
//移动构造函数
//参数是非const的右值引用
MyString(MyString && t)
{
str = t.str; //拷贝地址,没有重新申请内存
len = t.len;
//原来指针置空
t.str = NULL;
cout << "移动构造函数" << endl;
}
MyString &operator= (const MyString &tmp)
{//赋值运算符重载函数
if(&tmp == this)
{
return *this;
}
//先释放原来的内存
len = 0;
delete []str;
//重新申请内容
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);
cout << "赋值运算符重载函数 tmp.str = " << tmp.str << endl;
return *this;
}
//移动赋值函数
//参数为非const的右值引用
MyString &operator=(MyString &&tmp)
{
if(&tmp == this)
{
return *this;
}
//先释放原来的内存
len = 0;
delete []str;
//无需重新申请堆区空间
len = tmp.len;
str = tmp.str; //地址赋值
tmp.str = NULL;
cout << "移动赋值函数\n";
return *this;
}
~MyString()
{//析构函数
cout << "析构函数: ";
if(str != NULL)
{
cout << "已操作delete, str = " << str;
delete []str;
str = NULL;
len = 0;
}
cout << endl;
}
private:
char *str = NULL;
int len = 0;
};
MyString func() //返回普通对象,不是引用
{
MyString obj("mike");
return obj;
}
int main()
{
MyString tmp("abc"); //实例化一个对象
tmp = func();
return 0;
}
6. Função de biblioteca padrão std::move
O compilador só pode chamar o construtor de transferência e a função de atribuição de transferência para referências de rvalue,e todos os objectos nomeados só podem ser referências de lvalue.Se souber que um objecto nomeado já não é utilizado e quiser chamar o construtor de transferência e transferir a atribuição sobre ele Função, ou seja, usando uma referência lvalue como referência rvalue, como fazer? A biblioteca padrão fornece a função std::move, que converte uma referência lvalue em uma referência rvalue de uma forma muito simples.
int a;
int &&r1 = a; // 编译失败
int &&r2 = std::move(a); // 编译通过
7. Encaminhamento perfeito std::forward
O encaminhamento perfeito é adequado para cenários em que você precisa passar um conjunto de parâmetros para outra função inalterados. "Inalterado" significa não apenas que o valor do parâmetro permanece inalterado. Em C++, além do valor do parâmetro, existem dois conjuntos de atributos: lvalue/rvalue e const/non-const. O encaminhamento perfeito significa que durante o processo de passagem de parâmetros, todas essas propriedades e valores de parâmetros não podem ser alterados e, ao mesmo tempo, nenhuma sobrecarga adicional é incorrida, como se o encaminhador não existisse. Em funções genéricas, tal requisito é muito comum.
O exemplo a seguir ilustra:
#include <iostream>
using namespace std;
template <typename T> void process_value(T & val)
{
cout << "T &" << endl;
}
template <typename T> void process_value(const T & val)
{
cout << "const T &" << endl;
}
//函数 forward_value 是一个泛型函数,它将一个参数传递给另一个函数 process_value
template <typename T> void forward_value(const T& val)
{
process_value(val);
}
template <typename T> void forward_value(T& val)
{
process_value(val);
}
int main()
{
int a = 0;
const int &b = 1;
//函数 forward_value 为每一个参数必须重载两种类型,T& 和 const T&
forward_value(a); // T&
forward_value(b); // const T &
forward_value(2); // const T&
return 0;
}
Um parâmetro deve ser sobrecarregado duas vezes, ou seja, o número de sobrecargas de funções é diretamente proporcional ao número de parâmetros. O número de definições desta função é muito ineficiente para programadores.
Então, como o C++ 11 resolve o problema do encaminhamento perfeito? Na verdade, o C++ 11 alcança o encaminhamento perfeito introduzindo uma nova regra de linguagem chamada "recolhimento de referência" e combinando-a com novas regras de derivação de modelo.
typedef const int T;
typedef T & TR;
TR &v = 1; //在C++11中,一旦出现了这样的表达式,就会发生引用折叠,即将复杂的未知表达式折叠为已知的简单表达式
Regras de dobramento de referência em C++ 11:
Definição de tipo para TR |
declara o tipo de v |
o tipo real de v |
T& |
TR |
T& |
T& |
TR& |
T& |
T& |
TR && |
T& |
T&& |
TR |
T&& |
T&& |
TR& |
T& |
T&& |
TR && |
T&& |
Observe que uma vez que uma referência lvalue aparece em uma definição, a dobragem de referência sempre a dobra preferencialmente em uma referência lvalue. Em C++ 11, std::forward pode salvar as características lvalue ou rvalue dos parâmetros:
#include <iostream>
using namespace std;
template <typename T> void process_value(T & val)
{
cout << "T &" << endl;
}
template <typename T> void process_value(T && val)
{
cout << "T &&" << endl;
}
template <typename T> void process_value(const T & val)
{
cout << "const T &" << endl;
}
template <typename T> void process_value(const T && val)
{
cout << "const T &&" << endl;
}
//函数 forward_value 是一个泛型函数,它将一个参数传递给另一个函数 process_value
template <typename T> void forward_value(T && val) //参数为右值引用
{
process_value( std::forward<T>(val) );//C++11中,std::forward可以保存参数的左值或右值特性
}
int main()
{
int a = 0;
const int &b = 1;
forward_value(a); // T &
forward_value(b); // const T &
forward_value(2); // T &&
forward_value( std::move(b) ); // const T &&
return 0;
}