Novos recursos do C++ 11 ② | lvalue, referência lvalue, rvalue e referência rvalue

Índice

1. Introdução

2. Categorias de valor e conceitos relacionados

3. Lvalor, rvalor

4. referência de valor, referência de valor

5. Semântica Móvel

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...) icon-default.png?t=N7T8https://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...) icon-default.png?t=N7T8https://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...) icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795 Noções básicas e avançadas de C/C++ (artigo da coluna, atualizado continuamente...) icon-default.png?t=N7T8https://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;
}

Acho que você gosta

Origin blog.csdn.net/chenlycly/article/details/132746812
Recomendado
Clasificación