[C++]——Nuevas características de C++11 "referencia de valor y semántica de movimiento"

Prefacio:

  • En este número, presentaremos conocimientos relevantes sobre las referencias de valores de C++. Todos deben poder dominar el contenido del conocimiento de este tema, y ​​​​es un objetivo clave de inspección durante la entrevista.

Tabla de contenido

(1) Referencia de valor L y referencia de valor r

1. ¿Qué es un valor l? ¿Qué es una referencia de valor l?

2. ¿Qué es un valor? ¿Qué es una referencia de valor?

(2) Comparación de referencias de valor y referencias de valor

(3) Escenarios de uso e importancia de las referencias de valores

(4) Reenvío perfecto 

1. Concepto

2. La referencia universal && en la plantilla.

3、estándar::adelante

Resumir


(1) Referencia de valor L y referencia de valor r

La sintaxis tradicional de C++ tiene una sintaxis de referencia, y la nueva característica de sintaxis de referencia de rvalue se agrega en C++ 11, por lo que de ahora en adelante las referencias que aprendimos antes se denominan referencias de lvalue. Independientemente de si se trata de una referencia de valor l o de una referencia de valor rvalue, se le asigna un alias al objeto.

1. ¿Qué es un valor l? ¿Qué es una referencia de valor l?

Hay referencias en el estándar C++98/03, que están representadas por "&" . Sin embargo, este método de referencia tiene un defecto, es decir, en circunstancias normales, solo se pueden operar valores l en C ++ y no se pueden agregar referencias a valores r . Por ejemplo:
 

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

	return 0;
}

Pantalla de salida:

 【explicar】

  • Como se muestra arriba, el compilador nos permite crear una referencia al valor num l, pero no al valor 10. Por lo tanto, las referencias en el estándar C++ 98/03 también se denominan referencias de valor.
     

Entonces, ¿qué es exactamente un valor l? ¿Qué es una referencia de valor l?

  1. Un valor l es una expresión que representa datos (como un nombre de variable o un puntero desreferenciado). Podemos obtener su dirección + podemos asignarle un valor . Un valor l puede aparecer en el lado izquierdo del símbolo de asignación, y un valor r no puede aparecer en el lado izquierdo del símbolo de asignación. ;
  2. Al valor l después del modificador constante cuando se define no se le puede asignar un valor, pero se puede tomar su dirección. Una referencia de valor l es una referencia a un valor l y se le asigna un alias al valor l.
     

Nota : Aunque el estándar C++ 98/03 no admite el establecimiento de referencias de valores l no constantes a rvalues, sí permite el uso de referencias de valores l constantes para operar con rvalues. En otras palabras, una referencia de valor l constante puede operar tanto en valores l como en valores r, por ejemplo:
 

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. ¿Qué es un valor? ¿Qué es una referencia de valor?
 

Sabemos que los valores a menudo no tienen nombre , por lo que solo pueden usarse como referencia. Esto crea un problema. En el desarrollo real, es posible que necesitemos modificar el rvalue (requerido al implementar la semántica de movimiento). Obviamente, el método de referencia lvalue no funciona.


Con este fin, el estándar C++ 11 introduce otro método de referencia, llamado referencia rvalue, representada por "&&":

  • Un rvalue también es una expresión que representa datos , como: constante literal, valor de retorno de expresión, valor de retorno de función (esto no puede ser un retorno de referencia de valor l), etc.;
  • Un rvalue puede aparecer en el lado derecho de un símbolo de asignación, pero no puede aparecer en el lado izquierdo de un símbolo de asignación. Un rvalue no puede tomar una dirección ;
  • Una referencia de rvalue es una referencia a un rvalue, creando un alias para el 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;
}

Pantalla de salida:

 Pero si se trata de las siguientes expresiones, se producirá un error:

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

El resultado muestra:

Cabe señalar que no se puede obtener la dirección de un rvalue , pero darle un alias al rvalue hará que el rvalue se almacene en una ubicación específica y se
podrá obtener la dirección de esa ubicación. Por ejemplo, como se muestra en el siguiente código:

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

Pantalla de salida:

 Cuando no queremos que nos modifiquen, podemos agregar la palabra clave [const]:

【explicar】

  1. No se puede tomar la dirección del valor literal 10, pero después de hacer referencia a rr1, se puede tomar la dirección de rr1 y también se puede modificar rr1;
  2. Si no desea que se modifique rr1, puede usar const int&& rr1 para hacer referencia;
  3. ¿No se siente increíble? Comprender los escenarios de uso reales de las referencias de rvalue no radica en esto, y esta característica no es importante.
     

(2) Comparación de referencias de valor y referencias de valor

Resumen de referencia de valor:

  • 1. Las referencias de valores L solo pueden referirse a valores L, no a valores R.
  • 2. Sin embargo, una referencia de valor l constante puede referirse tanto a un valor l como a un valor r.

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

Pantalla de salida:

 Otro ejemplo es el siguiente:

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

    return 0;
}

Pantalla de salida:

 Una referencia de valor l solo puede hacer referencia a un valor l, no a un valor r. Pero cuando agregamos const, la referencia lvalue puede alias el rvalue:

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

Pantalla de salida:

【explicar】

Vale la pena mencionar que aunque la sintaxis de C++ admite la definición de referencias de rvalue constantes, dichas referencias de rvalue definidas no tienen ningún uso práctico:

const int& ra3 = 10;
  1. Por un lado, las referencias a rvalue se utilizan principalmente para la semántica de movimiento y el reenvío perfecto , donde la primera requiere permiso para modificar rvalues;
  2. En segundo lugar, la función de una referencia de valor r constante es hacer referencia a un valor r no modificable. Este trabajo se puede completar con una referencia de valor l constante.
     


Resumen de referencias de valores:

  • 1. Las referencias de Rvalue solo pueden referirse a rvalues, no a lvalues.
  • 2. Pero las referencias de rvalue pueden mover valores l posteriores.
     

Visualización de código:

 Se producirá un error al hacer referencia a un valor l:


La conversión de valores l a referencias rvalue se puede admitir mediante movimiento 

 En C++, moveuna plantilla de función que convierte un objeto determinado en la referencia rvalue correspondiente. No realiza la operación de movimiento de memoria real, pero marca el objeto como un valor que se puede mover. De esta manera, los usuarios pueden aprovechar este marcado para lograr una semántica de movimientos más eficiente.


(3) Escenarios de uso e importancia de las referencias de valores

Podemos ver anteriormente que las referencias de lvalue pueden referirse tanto a lvalues ​​como a rvalues, entonces, ¿por qué C++ 11 propone referencias de rvalue? ¿Es superfluo? ¡Echemos un vistazo a las deficiencias de las referencias de lvalue y cómo las referencias de rvalue compensan estas deficiencias!

Existe el siguiente código: 

 【explicar】

Primero, para res1 y res2 en el código anterior, son valores l y valores r respectivamente;

A continuación, pensemos en ello: ¿hay alguna diferencia entre copiar valores l y valores r?

  • Si es un tipo integrado, la diferencia entre ellos no es muy grande, pero para un tipo personalizado, la diferencia es muy grande.
  • Debido al valor del tipo personalizado, generalmente se le denomina valor de muerte en muchos lugares. Generalmente es el valor de retorno de algunas expresiones, una llamada a función, etc.;
  • En cuanto a los valores, se dividen en valores puros (en términos generales, tipos integrados) y valores moribundos (en términos generales, tipos personalizados).

Para el res1 anterior, es un valor l, no podemos operar con él, solo podemos hacer una copia profunda. Porque aunque aquí parezca una tarea, en realidad debería ser una construcción de copia;

En cuanto a res2, es un rvalue en sí mismo, si es un tipo autodefinido como valor, no necesitamos copiarlo. En este punto, se introduce el concepto de construcción basada en la implementación de referencias de valores.


Por ejemplo, ahora hay una cadena que simulamos escribiendo a mano:

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

Cuando observamos los res1 y res2 anteriores en este escenario:

 Podemos encontrar que el valor aquí es una copia profunda correspondiente, lo que obviamente causa un desperdicio innecesario. Para resolver los problemas anteriores, podemos introducir el concepto de "construcción de movimientos":

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

Inmediatamente después, al ejecutar nuevamente el código anterior, podemos encontrar que el compilador reconocerá automáticamente:

En este punto, ¿qué podemos hacer cuando solo queremos convertir s1 en un valor r? En realidad es muy simple ( el movimiento se refleja aquí ):

 Pantalla de salida:

También podemos encontrar mediante la depuración que el efecto esperado se logra en este momento:

 【resumen】

De lo anterior podemos encontrar que el beneficio de las referencias de valor es reducir directamente la copia.

Los escenarios de uso de las referencias de lvalue se pueden dividir en las dos partes siguientes:

  • Tanto los parámetros como los valores de retorno pueden mejorar la eficiencia.

Deficiencias de las referencias de lvalue:

  • Pero cuando el objeto de retorno de la función es una variable local, no existe fuera del alcance de la función, por lo que no puede usar la referencia lvalue para regresar y solo puede regresar por valor.

Por ejemplo, ahora tenemos el siguiente 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 se puede ver en la función zp::string to_string(int value), aquí solo se puede usar return by value. Return by value dará como resultado al menos una construcción de copia (tal vez dos construcciones de copia si se trata de algunos compiladores más antiguos ) .

 A continuación, imprimamos para ver cuál es el resultado:

 En este momento, el coste de devolución por valor se ha solucionado en gran medida:

 Las referencias de Rvalue y la semántica de movimiento resuelven los problemas anteriores:

  • Agregue una construcción de movimiento a zp::string. La esencia de la construcción de movimiento es robar los recursos del parámetro rvalue. Si el marcador de posición ya está allí, entonces no hay necesidad de hacer una copia profunda, por lo que se llama movimiento. construcción. Significa robar los recursos de otras personas para construir los tuyos propios. ;

Si ejecutamos las dos llamadas a zp :: to_string anteriores, encontraremos que aquí no se llama a la estructura de copia de copia profunda, pero se llama a la estructura de movimiento. No hay espacio nuevo para abrir y copiar datos en la estructura de movimiento. por lo que se mejora la eficiencia.


C++ 11 no solo tiene construcción de movimientos, sino también asignación de movimientos:

Agregue una función de asignación de movimiento a la clase zp::string y luego llame a zp::to_string(1234), pero esta vez el objeto rvalue devuelto por zp::to_string(1234) se asigna al objeto ret1. En este momento, el movimiento se llama estructura.

Pantalla de salida:

 【explicar】

  • Después de ejecutar esto, vemos que se llama a un constructor de movimientos y a una asignación de movimientos. Porque si se utiliza un objeto existente para recibirlo, el compilador no puede optimizarlo. La función zp::to_string usará primero la construcción de generación str para generar un objeto temporal, pero podemos ver que el compilador es lo suficientemente inteligente como para reconocer str como un valor r y llamar a la construcción move. Luego use este objeto temporal como valor de retorno de la llamada a la función zp::to_string y asígnelo a ret1. La asignación de movimiento llamada aquí.

(4) Reenvío perfecto 

1. Concepto

  • El reenvío perfecto es una característica introducida en C++ 11, cuyo objetivo es lograr la capacidad de pasar con precisión tipos de parámetros en plantillas de funciones;
  • Se utiliza principalmente para preservar la categoría de valor de los parámetros reales pasados ​​a la plantilla de función y reenviarla a la función llamada internamente, logrando así la preservación completa del tipo y la categoría de valor;

Por ejemplo:

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

【explicar】

  1. Como se muestra arriba, la función Func() se llama en la plantilla de función PerfectForward();
  2. Sobre esta base, el reenvío perfecto se refiere a: si el parámetro t recibido por la función PerfectForward () es un valor l, entonces el parámetro t pasado por la función a Func () también es un valor l;
  3. Por otro lado, si el parámetro t recibido por la función function() es un rvalue, entonces el parámetro t pasado a la función Func() también debe ser un rvalue.

Utilizando cualquiera de las formas de cita, se puede lograr el reenvío, pero no se garantiza la perfección. Por lo tanto, si usamos el lenguaje C++ bajo el estándar C++ 98/03, podemos usar la sobrecarga de plantillas de funciones para lograr un reenvío perfecto, por ejemplo:

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

Pantalla de salida:

 【explicar】

  1. En el ejemplo anterior, definimos dos plantillas de funciones sobrecargadas  Func, una que recibe parámetros de referencia de valor l T& argy la otra que recibe parámetros de referencia directa.T&& arg;
  2. Luego, definimos una función de plantilla PerfectForwardcuyos parámetros también son referencias reenviadas T&& arg. Dentro PerfectForwardde la función, Funcprocesamos los parámetros pasados ​​llamando a la función;
  3. A través del mecanismo de sobrecarga de funciones, los parámetros lvalue pasados ​​coincidirán con la función que recibe la referencia lvalue Func, y el parámetro rvalue pasado coincidirá Funccon la función que recibe la referencia reenviada, para que puedan distinguirse y procesarse correctamente;
  4. Mediante la sobrecarga de plantillas de funciones, podemos distinguir valores l y valores r según los tipos de parámetros y procesarlos por separado, logrando coincidencias y operaciones precisas para diferentes categorías de valores.

2. La referencia universal && en la plantilla.

Obviamente, el uso de funciones de plantilla sobrecargadas para lograr un reenvío perfecto mencionado anteriormente también tiene desventajas. Este método de implementación solo es adecuado para situaciones en las que la función de plantilla tiene solo unos pocos parámetros. De lo contrario, será necesario crear una gran cantidad de plantillas de funciones sobrecargadas. escrito, lo que provoca redundancia de código. Para facilitar que los usuarios logren un reenvío perfecto más rápidamente, el estándar C++ 11 permite el uso de referencias rvalue en plantillas de funciones para lograr un reenvío perfecto.

Tomemos como ejemplo la función PerfectForward(). Para lograr un reenvío perfecto en el estándar C++11, solo necesita escribir la siguiente función de plantilla:

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

Tome el siguiente código como ejemplo:

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

Pantalla de salida:

 【explicar】

  1. El && en la plantilla no representa una referencia de rvalue, sino una referencia universal, que puede recibir tanto lvalues ​​como rvalues.
  2. La referencia universal de la plantilla solo brinda la capacidad de recibir referencias de lvalue y rvalue.
  3. Sin embargo, la única función de los tipos de referencia es limitar los tipos recibidos y degeneran en valores l en usos posteriores.
  4. Si queremos poder mantener sus atributos lvalue o rvalue durante el proceso de transferencia, debemos utilizar el reenvío perfecto que aprenderemos a continuación.
     

3、estándar::adelante

Los desarrolladores del estándar C++ 11 ya han pensado en una solución para nosotros. El nuevo estándar también introduce una función de plantilla forword<T>(). Sólo necesitamos llamar a esta función para resolver fácilmente este problema.

  1. El reenvío perfecto generalmente se usa con referencia de reenvío y la función std::forward;
  2. La referencia reenviada es un tipo de referencia especial, &&declarada mediante sintaxis, que se utiliza para capturar los parámetros reales pasados ​​en la plantilla de función;
  3. std::forward es una función de plantilla utilizada para reenviar referencias como referencias rvalue o lvalue dentro de una plantilla de función.

A continuación se demuestra el uso de esta plantilla de función:

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

El resultado de la ejecución del programa es:

Mediante el reenvío perfecto, podemos manejar correctamente la categoría de valor del parámetro real pasado en la plantilla de función y reenviarlo a la función interna para lograr la preservación completa del tipo y la categoría de valor, mejorando la flexibilidad y eficiencia del código.


Resumir

Después de aprender esto, es posible que algunos lectores no puedan recordar claramente si las referencias de lvalue y rvalue pueden referirse a lvalues ​​o rvalues. Aquí hay una tabla para que todos puedan facilitar su memoria:
 

  •  En la tabla, Y significa compatible y N significa no compatible.

¡Lo anterior es todo el conocimiento sobre las referencias de lvalue y rvalue! ¡Gracias a todos por mirar y apoyar! ! !

Supongo que te gusta

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