Nuevas características de C++ 11 ② | lvalue, referencia lvalue, rvalue y referencia rvalue

Tabla de contenido

1. Introducción

2. Categorías de valores y conceptos relacionados

3. Valor izquierdo, valor derecho

4. referencia de valor l, referencia de valor r

5. Semántica móvil

5.1. Por qué es necesaria la semántica de movimientos

5.2 Definición de semántica móvil

5.3 Constructor de transferencia

5.4 Función de asignación de transferencia

6. Función de biblioteca estándar std::move

7. Reenvío perfecto std::forward


Resumen de desarrollo de funciones comunes de VC ++ (lista de artículos en columna, bienvenido a suscribirse, actualización continua...) icono-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585 Serie de tutoriales sobre solución de problemas de excepciones de software C ++ desde el nivel inicial hasta el dominio (artículo en columna lista, bienvenido a suscribirse, seguir actualizando...) icono-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931 Herramientas de análisis de software C++ desde la entrada hasta la recopilación de casos de dominio (el artículo de la columna se está actualizando...) icono-default.png?t=N7T8https :/ /blog.csdn.net/chenlycly/article/details/131405795 Conceptos básicos y avanzados de C/C++ (artículos en columnas, actualizados continuamente...) icono-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_11931267.html        C++ 11 Nuevo Las características son muy importantes. Como desarrollador de C ++, es necesario aprender. No solo participará en la entrevista de prueba escrita, sino que también se utilizará a gran escala en código fuente abierto. Tome como ejemplo el proyecto WebRTC de código abierto utilizado por muchos software de videoconferencia y transmisión en vivo. Las nuevas características de C ++ 11 y superiores se utilizan ampliamente en el código WebRTC. Para comprender su código fuente, debe comprender estas nuevas características de C++. Por lo tanto, en el próximo período de tiempo, combinaré mi práctica laboral para explicar en detalle las nuevas características de C ++ 11 como referencia o referencia.

1. Introducción

       C ++ 11 introduce el concepto de movimiento de objetos, que es la capacidad de mover objetos en lugar de copiarlos, y mover objetos puede mejorar efectivamente el rendimiento del programa.

       Para admitir operaciones de movimiento, C++ 11 introduce un nuevo tipo de referencia: referencias rvalue. La llamada referencia rvalue es una referencia que debe estar vinculada a un rvalue y la referencia rvalue se obtiene a través del operador &&. Hoy, hablemos en detalle sobre valores l, referencias de valores l, valores r y referencias de valores r.

2. Categorías de valores y conceptos relacionados

       En C ++ 11, las categorías de valores se dividen principalmente en lvalues, referencias de lvalue, rvalues ​​​​y referencias de rvalue. Una referencia de valor l es una aplicación vinculada a un valor l y una referencia de valor r es una referencia vinculada a un valor r. Cuando la referencia de valor T&& aparece en el parámetro de una función de plantilla, T es el tipo de plantilla y T&& es una referencia universal. Las referencias universales pueden recibir parámetros lvalue y rvalue. Entonces puede ocurrir un plegado de referencia cuando se deducen los tipos de parámetros de función. También participarán las funciones de biblioteca estándar std::move y std::forward.

3. Valor izquierdo, valor derecho

       En lenguaje C, a menudo mencionamos lvalue (lvalue) y rvalue (rvalue). Uno de los métodos de identificación más típicos es que en una expresión de asignación, lo que aparece en el lado izquierdo del signo igual es un "valor l" y lo que aparece en el lado derecho del signo igual se llama "valor r". como:

int b = 1;
int c = 2;
int a = a + b;

En esta expresión de asignación, a es un valor l y b + c es un valor r.

       Sin embargo, hay otro dicho ampliamente reconocido en C++, es decir, aquellos que pueden tomar direcciones y tener nombres son valores l, y a la inversa, aquellos que no pueden tomar direcciones y no tienen nombres son valores r. Luego, en esta expresión de asignación de suma, &a es una operación permitida, pero operaciones como &(b + c) no se compilarán. Por lo tanto a es un valor l y (b + c) es un valor r. En relación con los valores l, los valores r representan constantes literales, expresiones, valores de retorno de funciones sin referencia, etc.

       Para resumir aquí, ¿qué es un valor l?

1) Se puede asignar un valor (condiciones suficientes pero no necesarias), no necesariamente se pueden asignar valores l, como variables constantes, la inteligencia asigna un valor inicial durante la inicialización y no se puede asignar posteriormente 2) Puede acceder a direcciones (suficientes pero no necesarias
) condiciones no necesarias), los valores l pueden no ser Puede tomar direcciones, como la variable de registro en lenguaje C: registro int i = 3. C ++ 11 ha cancelado el soporte para registro y será ignorado durante la compilación. Para otro ejemplo, las variables de campo de bits en lenguaje C no se pueden abordar:
struct St{ int m:3;} St st; st.m = 3; especifica que el miembro de tipo int m ocupa 3 bytes.
3) Se puede inicializar una referencia de lvalue (una condición necesaria pero no suficiente). Un lvalue puede inicializar una referencia de lvalue, por ejemplo: int m = 3; int& n = m;, y un rvalue no puede inicializar una referencia de lvalue, por ejemplo : const int& m = 3; debido a que 3 es un valor r, la referencia de valor l no se puede inicializar, por lo que la compilación informará un error.
4) Los literales son valores r puros. Por ejemplo, los números inmediatos como 1, 2 y 3 son literales. Tenga en cuenta que la cadena constante "xyz" es un valor l y se puede tomar la dirección de esta cadena, es decir, &" xyz".
5) Los recursos en valor moribundo pueden ser robados. El valor de retorno de la función es un rvalue puro y devuelve una referencia de rvalue, como la función std::move.

4. referencia de valor l, referencia de valor r

       Una referencia de valor l es un tipo que hace referencia a un valor l y una referencia de valor r es un tipo que hace referencia a un valor r. Tanto las referencias de lvalue como las de rvalue son tipos de referencia. Ya sea que se declare una referencia de lvalue o una referencia de rvalue, debe inicializarse inmediatamente. La razón puede entenderse como que el tipo de referencia en sí no posee la memoria del objeto vinculado, sino que es solo un alias del objeto.

        Una referencia lvalue es un alias para un valor de variable con nombre, mientras que una referencia rvalue es un alias para una variable sin nombre (anónima).

        Ejemplo de referencia de valor l:

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&" es un tipo de referencia "universal", que puede aceptar valores l no constantes, valores l constantes y valores r para la inicialización;

       referencia de valor, representada por &&:

int && r1 = 22;
int x = 5;
int y = 8;
int && r2 = x + y;
T && a = ReturnRvalue();

       Normalmente, las referencias de rvalue no se pueden vincular a ningún lvalue:

int c;
int && d = c; //err

       Veamos un ejemplo de prueba:

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óvil

5.1. Por qué es necesaria la semántica de movimientos

       Las referencias de Rvalue se utilizan para respaldar la semántica de transferencia. La semántica de transferencia puede transferir recursos (montón, objetos del sistema, etc.) de un objeto a otro, lo que puede reducir la creación, copia y destrucción de objetos temporales innecesarios y puede mejorar en gran medida el rendimiento de las aplicaciones C ++. El mantenimiento (creación y destrucción) de objetos temporales tiene un grave impacto en el rendimiento.

       La semántica de transferencia es opuesta a la semántica de copia y se puede comparar con cortar y copiar archivos: cuando copiamos un archivo de un directorio a otro, la velocidad es mucho más lenta que la de cortar. Mediante la semántica de transferencia, los recursos de los objetos temporales se pueden transferir a otros objetos.

5.2 Definición de semántica móvil

       En el mecanismo C++ existente, podemos definir constructores de copia y funciones de asignación. Para implementar la semántica de transferencia, debe definir un constructor de transferencia y también puede definir un operador de asignación de transferencia. Para copiar y asignar valores, se llama al constructor de transferencia y al operador de asignación de transferencia.

       Si el constructor de transferencia y el operador de copia de transferencia no están definidos, se sigue el mecanismo existente y se llamará al constructor de copia y al operador de asignación. Las funciones y operadores ordinarios también pueden utilizar operadores de referencia rvalue para implementar la semántica de transferencia.

5.3 Constructor de transferencia

       Primero veamos un ejemplo de constructor de transferencia:

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;
}


Al igual que con el constructor de copias, hay algunos puntos a tener en cuenta:

1) El símbolo del parámetro (rvalue) debe ser un símbolo de referencia de rvalue, es decir, "&&".
2) El parámetro (rvalue) no puede ser una constante, porque necesitamos modificar el rvalue.
3) Los enlaces de recursos y las etiquetas de los parámetros (rvalue) deben modificarse; de ​​lo contrario, el destructor de rvalue liberará los recursos y los recursos transferidos al nuevo objeto no serán válidos.

       Con la referencia de rvalue y la semántica de transferencia, cuando diseñamos e implementamos clases, para clases que necesitan solicitar dinámicamente una gran cantidad de recursos, debemos diseñar constructores de transferencia y funciones de asignación de transferencia para mejorar la eficiencia de la aplicación. 

5.4 Función de asignación de transferencia

         Veamos directamente el ejemplo de la función de asignación de transferencia:

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. Función de biblioteca estándar std::move

       El compilador solo puede llamar al constructor de transferencia y a la función de asignación de transferencia para referencias de rvalue, y todos los objetos con nombre solo pueden ser referencias de lvalue. Si sabe que un objeto con nombre ya no se usa y desea llamar al constructor de transferencia y transferir la asignación en él Función, es decir, usar una referencia lvalue como referencia rvalue, ¿cómo se hace? La biblioteca estándar proporciona la función std::move, que convierte una referencia lvalue en una referencia rvalue de una manera muy sencilla.

int a;
int &&r1 = a;              // 编译失败
int &&r2 = std::move(a);      // 编译通过

7. Reenvío perfecto std::forward

        El reenvío perfecto es adecuado para escenarios en los que es necesario pasar un conjunto de parámetros intactos a otra función. "Sin cambios" no significa solo que el valor del parámetro permanece sin cambios. En C++, además del valor del parámetro, hay dos conjuntos de atributos: lvalue/rvalue y const/non-const. El reenvío perfecto significa que durante el proceso de transferencia de parámetros, todas estas propiedades y valores de parámetros no se pueden cambiar y, al mismo tiempo, no se produce una sobrecarga adicional, como si el reenviador no existiera. En funciones genéricas, estos requisitos son muy comunes.

       Aquí hay unos ejemplos:

#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;
}

        Un parámetro debe sobrecargarse dos veces, es decir, el número de sobrecargas de funciones es directamente proporcional al número de parámetros. La cantidad de definiciones de esta función es muy ineficiente para los programadores.

        Entonces, ¿cómo resuelve C++ 11 el problema del reenvío perfecto? De hecho, C++ 11 logra un reenvío perfecto al introducir una nueva regla de lenguaje llamada "colapso de referencia" y combinarla con nuevas reglas de derivación de plantillas.

typedef const int T;
typedef T & TR;
TR &v = 1; //在C++11中,一旦出现了这样的表达式,就会发生引用折叠,即将复杂的未知表达式折叠为已知的简单表达式

        Reglas de plegado de referencia en C++11:

Definición de tipo de TR

Declarar el tipo de v

tipo real de v

T &

TR

T &

T &

TR y

T &

T &

TR &&

T &

T&&

TR

T&&

T&&

TR y

T &

T&&

TR &&

T&&

Tenga en cuenta que una vez que aparece una referencia de valor l en una definición, el plegado de referencia siempre la colapsa primero en una referencia de valor l. En C++11, std::forward puede guardar las características lvalue o rvalue de los 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;
}

Guess you like

Origin blog.csdn.net/chenlycly/article/details/132746812