[C++] C++ 11 (referencia de valor, semántica de movimiento, enlace, contenedor, lambda, biblioteca de subprocesos)

1. Introducción a C++11

C++ 11 es una importante versión actualizada del lenguaje C++. Se lanzó en 2011. Contiene algunas características nuevas muy útiles, que brindan a los desarrolladores mejores herramientas de programación y una mejor experiencia de programación, lo que hace que la escritura sea eficiente y confiable. es mas facil.

Algunas de las nuevas características de C++11 incluyen:

  1. Las enumeraciones de tipos forzados hacen que el comportamiento habitual de los tipos enumerados sea más confiable y fácil de controlar.
  2. La inferencia automática de tipos (auto) puede inferir automáticamente tipos de variables en función de las condiciones reales, lo que hace que el código sea más conciso y más fácil de leer.
  3. Las expresiones Lambda, que admiten funciones anónimas, proporcionan una forma sencilla y potente de definir y utilizar objetos de función.
  4. La deducción global de tipos de funciones puede inferir automáticamente los tipos de retorno de funciones para evitar definiciones repetidas.
  5. Las referencias de Rvalue pueden mejorar el rendimiento y la eficiencia del programa, al tiempo que implementan mejor algunas técnicas de programación avanzadas, como la semántica de movimiento.
  6. Biblioteca de concurrencia diseñada para hacer que la escritura de programas multiproceso sea más fácil y segura.

Además, C++ 11 también introduce muchas otras características nuevas, como punteros inteligentes, funciones de eliminación predeterminadas, operadores de conversión explícitos e iteradores de rango.

Desde C ++ 0x hasta C ++ 11, el estándar C ++ ha estado funcionando durante 10 años y el segundo estándar verdadero llegó tarde. En comparación con C++ 98/03, C++ 11 ha traído una cantidad considerable de cambios, incluidas alrededor de 140 características nuevas y alrededor de 600 correcciones de errores en el estándar C++ 03, lo que hace que C ++ 11 se parezca más a un Nuevo lenguaje concebido a partir de C++98/03. En comparación, C++ 11 se puede utilizar mejor para el desarrollo de sistemas y bibliotecas, su sintaxis es más generalizada y simplificada, más estable y más segura, no solo tiene funciones más poderosas, sino que también puede mejorar la eficiencia del desarrollo de los programadores. También se usa mucho en el desarrollo de proyectos, por lo que tenemos que aprenderlo como un punto clave .

Con todo, C++ 11 es una gran mejora para el lenguaje C++, ya que nos proporciona mejores herramientas de programación y una forma más eficiente, legible y mantenible de escribir código. Por lo tanto, aprender C++ 11 es un beneficio para toda la vida.

Documentación oficial de C ++ 11

cuento:

1998 fue el primer año del establecimiento del Comité de Estándares C++. Originalmente se planeó actualizar el estándar cada cinco años dependiendo de las necesidades reales. Cuando el Comité de Estándares Internacionales C++ estaba estudiando la próxima versión de C++03, originalmente Su lanzamiento estaba previsto para 2007, por lo que inicialmente este estándar se llama C++07. Pero en 2006, el funcionario consideró que C ++ 07 definitivamente no se completaría en 2007, y el funcionario consideró que tal vez no se completaría en 2008. Al final, se llamó simplemente C++0x. x significa que no sé si se podrá completar en 2007, 2008 o 2009. Como resultado, no se completó en 2010 y finalmente el estándar C ++ se completó en 2011. Así que finalmente se llamó C++11. (De hecho, Java es más diligente a este respecto)

2. Inicialización de lista unificada

2.1 {}Inicialización

En C++98, el estándar permite el uso de llaves {} para la inicialización uniforme de la lista de elementos de matriz o estructura. Por ejemplo:

struct Point
{
     
     
    int _x;
    int _y;
};
int main()
{
     
     
    int array1[] = {
     
      1, 2, 3, 4, 5 };
    int array2[5] = {
     
      0 };
    Point p = {
     
      1, 2 };
    return 0;
}

C ++ 11 amplía el uso de listas encerradas entre llaves (listas de inicialización) para que puedan usarse para todos los tipos integrados y tipos definidos por el usuario. Al usar listas de inicialización, puede agregar un signo igual (=), o No es necesario agregar .

struct Point
{
     
     
    int _x;
    int _y;
};
int main()
{
     
     
    int x1 = 1;
    int x2{
     
      2 };
    int array1[]{
     
      1, 2, 3, 4, 5 };
    int array2[5]{
     
      0 };
    Point p{
     
      1, 2 };
    // C++11中列表初始化也可以适用于new表达式中
    int* pa = new int[4]{
     
      0 };
    return 0;
}

Por supuesto, al crear un objeto, también puede utilizar la inicialización de lista para llamar a la inicialización del constructor.

class Date
{
     
     
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
     
     
		cout << "Date(int year, int month, int day)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
     
     
	Date d1(2022, 1, 1);
	// C++11支持的列表初始化,这里会调用构造函数初始化
	Date d2{
     
      2022, 1, 2 };
	Date d3 = {
     
      2022, 1, 3 };
	return 0;
}
imagen-20230502200212117

2.2 std::initializer_list

Documentación introductoria para std::initializer_list

¿Qué tipo es std::initializer_list?

int main()
{
     
     
	auto il = {
     
      1,3,2,5 };
	cout << typeid(il).name() << endl;//class std::initializer_list<int>
	return 0;
}
imagen-20230502202559472

std::initializer_list escenarios de uso :

std::initializer_list se usa generalmente como parámetro del constructor. C++ 11 agrega std::initializer_list como constructor de parámetros a muchos contenedores en STL, lo que hace que sea más conveniente inicializar objetos contenedores. También se puede utilizar como parámetro de operador =, de modo que se pueda asignar entre llaves.

La siguiente es una introducción a la documentación del constructor de algunos contenedores. La mayoría de ellos admiten la construcción std::initializer_list.

lista

vector

mapa

Esto será muy conveniente para los contenedores de mapas.

Por ejemplo:

int main()
{
     
     
	vector<int> v = {
     
      1,2,3,4 };
	list<int> lt = {
     
      1,2 };
	// 这里{"sort", "排序"}会先初始化构造一个pair对象
	map<string, string> dict = {
     
      {
     
     "sort", "排序"}, {
     
     "insert", "插入"} };
	// 使用大括号对容器赋值
	v = {
     
      10, 20, 30 };
	return 0;
}

Deje que el vector simulado también admita {} inicialización y asignación

vector(std::initializer_list<T> il)
    :_start(nullptr)
    , _finish(nullptr)
    , _end_of_storage(nullptr)
{
     
     
    for (const auto& e : il)
    {
     
     
    	push_back(e);
    }
}

3. Declaración

C++ 11 proporciona varias formas de simplificar las declaraciones, especialmente cuando se utilizan plantillas.

3.1 automático

En C++98, auto es un especificador de tipo de almacenamiento, lo que indica que la variable es un tipo de almacenamiento automático local. Sin embargo, las variables locales definidas en un dominio local tienen por defecto un tipo de almacenamiento automático, por lo que auto no tiene valor. En C ++ 11, el uso original de auto se descarta y se utiliza para realizar una evaluación automática de tipos. Esto requiere una inicialización explícita, lo que permite al compilador establecer el tipo del objeto definido en el tipo del valor inicializado .

int main()
{
     
     
	int i = 10;
	auto p = &i;// 推导为int*类型
	auto pf = "apple";//推导为const char* 类型
	cout << typeid(p).name() << endl;
	cout << typeid(pf).name() << endl;
	map<string, string> dict = {
     
      {
     
     "apple", "苹果"}, {
     
     "banana", "香蕉"} };
	//map<string, string>::iterator it = dict.begin();
	auto it = dict.begin();
	return 0;
}
imagen-20230502210018918

3.2 tipo de declive

La palabra clave decltype declara que el tipo de una variable es el tipo especificado por la expresión.

// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
     
     
	decltype(t1 * t2) ret;
	cout << typeid(ret).name() << endl;
}
int main()
{
     
     
	const int x = 1;
	double y = 2.2;
	decltype(x * y) ret; // ret的类型是double
	decltype(&x) p; // p的类型是int*
	cout << typeid(ret).name() << endl;
	cout << typeid(p).name() << endl;
	F(1, 'a');//int 和 char --》int
	return 0;
}
imagen-20230502212220198

3.3 La diferencia entre auto y decltype

decltype y auto son dos nuevas palabras clave proporcionadas por C++ 11. Su función es permitir que el compilador infiera automáticamente el tipo de variable.

auto se puede utilizar para la deducción automática de tipos de variables locales y valores de retorno de funciones, y el compilador deducirá el tipo de variable según el tipo de expresión. Por ejemplo:

auto i = 10; // 推导为int类型
auto s = "hello"; // 推导为const char*类型

Y decltype se usa para obtener el tipo de expresión, incluido el tipo de valor de retorno de variables y expresiones. Por ejemplo:

int i = 10;
decltype(i) j; // 推导类型为int

double getValue();
decltype(getValue()) d; // 推导类型为double

Como puede ver, decltype necesita especificar una expresión o nombre de variable cuando lo usa, pero auto no. Además, el tipo de valor de retorno de decltype tiene un tipo completamente preciso, incluidos los calificadores const, reference y cv, etc., mientras que auto solo puede deducir tipos desnudos, y se requieren derivación de tipos y modificadores constantes para deducir el tipo completo.

En resumen, tanto decltype como auto pueden usarse para deducir tipos de variables, pero sus funciones son ligeramente diferentes. Auto se usa principalmente para la deducción automática de tipos de variables locales y valores de retorno de funciones, mientras que decltype se usa para obtener el tipo exacto de expresiones, incluidas const, referencia y otros calificadores.

3.4 punto nulo

Dado que NULL en C++ se define como literal 0, esto puede causar algunos problemas, porque 0 puede representar tanto una constante de puntero como una constante entera. Por lo tanto, en aras de la claridad y la seguridad, se agrega nullptr en C++ 11 para representar un puntero nulo.

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

4. Referencias de valores y semántica de movimientos

4.1 referencias de valores y referencias de valores

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.

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

valor lEs una expresión que representa datos (como un nombre de variable o un puntero desreferenciado). Podemos obtener su dirección + podemos asignarle un valor. El valor l puede aparecer en el lado izquierdo del símbolo de asignación, y el valor r no puede aparecen en el lado izquierdo del símbolo de asignación . 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.

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

valorTambién es una expresión que representa datos, como: constantes literales, valores de retorno de expresión, valores de retorno de función (esto no puede ser un retorno de referencia de valor), etc. Los valores r pueden aparecer en el lado derecho del símbolo de asignación. pero no puede aparecer en el símbolo de asignación. En el lado izquierdo de, el valor r no puede tomar la dirección. Una referencia de rvalue es una referencia a un rvalue, que le da un alias al 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);
	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;
	return 0;
}

imagen-20230503151831224

Cabe señalar que la dirección de un rvalue no se puede tomar, pero darle un alias al rvalue hará que el rvalue se almacene en una ubicación específica y se pueda obtener la dirección de esa ubicación, es decir, por ejemplo : No se puede tomar la dirección literal 10, pero después de hacer referencia a rr1, se puede obtener la dirección de rr1 o se puede modificar rr1. Si no desea que se modifique rr1, puede usar const int&& rr1 para hacer referencia a él. ¿No se siente increíble? Entendamos que el escenario de uso real de la referencia de rvalue no se encuentra en esto y que esta característica no es importante. .

int main()
{
     
     
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;
	rr1 = 20;
	rr2 = 5.5; // 报错
	return 0;
}

imagen-20230503152002767

4.2 Comparación de referencias de valor y referencias de valor

Resumen de referencia de valor :

  1. Una referencia de valor l solo puede hacer referencia a un valor l, no a un valor r.

  2. Pero las referencias constantes de valores l pueden referirse tanto a valores l como a valores r.

int main()
{
     
     
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a; // ra为a的别名
	//int& ra2 = 10; // 编译失败,因为10是右值
	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

Resumen de referencia de Rvalue :

  1. Las referencias de Rvalue solo pueden ser rvalues, no lvalues.

  2. Pero las referencias de rvalue pueden mover valores l posteriores.

int main()
{
     
     
	// 右值引用只能右值,不能引用左值。
	int&& r1 = 10;
	// error C2440: “初始化”: 无法从“int”转换为“int &&”
	// message : 无法将左值绑定到右值引用
	int a = 10;
	int&& r2 = a;//报错:“初始化”: 无法从“int”转换为“int &&”
	// 右值引用可以引用move以后的左值
	int&& r3 = std::move(a);
	return 0;
}

imagen-20230503200647061

4.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!

Escenarios de uso de referencias de valor l :
tanto la creación de parámetros como la generación de valores de retorno pueden mejorar la eficiencia .

void func1(hdm::string s)
{
     
     }
void func2(const hdm::string& s)
{
     
     }
int main()
{
     
     
	hdm::string s1("hello world");
	// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
	func1(s1);
	func2(s1);
	// string operator+=(char ch) 传值返回存在深拷贝
	// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
	s1 += '!';
	return 0;
}

imagen-20230828194828062

Nota: Necesitamos usar nuestra propia cadena simulada para habilitar la impresión. Solo necesitamos agregar un código de entrada al realizar una copia profunda. El siguiente ejemplo es el mismo.

//现代写法
string& operator=(string s)
{
     
     
   swap(s);
   cout << "string& operator=(string s) --- 深拷贝" << endl;
   return *this;
}

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: como puede ver en la función hdm::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 (si se trata de algún compilador anterior, puede ser dos construcciones de copia) ).

imagen-20230828195933107

imagen-20230828195914489

imagen-20230828200447778

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

Agregue una construcción de movimiento a hdm::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 eso se llama movimiento construir, lo que significa robar los recursos de otras personas para construir los tuyos propios .

string(string&& s)
:_str(nullptr), _capacity(0), _size(0)
{
     
     
   cout << "string(string&& s)---移动构造" << endl;
   swap(s);
}

Si ejecutamos las dos llamadas a hdm::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.

imagen-20230828200724962

imagen-20230828200851909

No solo mueva la construcción, sino también mueva la asignación:

Agregue una función de asignación de movimiento a la clase bit::string y luego llame a hdm::to_string(1234), pero esta vez el objeto rvalue devuelto por hdm::to_string(1234) se asigna al objeto ret1. En este momento, la llamada es construcción móvil

string& operator=(string&& s)
{
     
     
   cout << "string operator=(string&& s)---移动赋值" << endl;
   swap(s);
   return *this;
}

int main()
{
     
     
	hdm::string ret1;
	ret1 = hdm::to_string(1234);
	return 0;
}
// 运行结果:
// string(string&& s) -- 移动构造
// string& operator=(string&& s) -- 移动赋值

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. En la función hdm::to_string, se generará un objeto temporal con la construcción de generación str primero, pero podemos ver que el compilador es muy inteligente aquí al reconocer str como un valor r y llamar a la construcción move. Luego asigne este objeto temporal como valor de retorno de la llamada a la función hdm::to_string a ret1, y la asignación de movimiento llamada aquí.

Solo en el siguiente caso el compilador lo optimizará directamente en una construcción de movimiento

int main()
{
     
     
	hdm::string ret1 = hdm::to_string(1234);
	return 0;
}
// 运行结果:
// string(string&& s) -- 移动构造

4.4 Las referencias de Rvalue se refieren a lvalues ​​​​y algunos análisis en profundidad de escenarios de uso.

Según la sintaxis, las referencias de rvalue solo pueden hacer referencia a rvalues, pero ¿las referencias de rvalue no deben hacer referencia a lvalues?

Porque: en algunos escenarios, puede ser realmente necesario utilizar un valor r para hacer referencia a un valor l para lograr la semántica de movimiento. Cuando necesite usar una referencia de rvalue para hacer referencia a un valor de l, puede usar la función de movimiento para convertir el valor de l en un valor de r. En C++ 11, la función std::move() se encuentra en el archivo de encabezado. El nombre de la función es confuso. No mueve nada. La única función es convertir un valor l en una referencia rvalue y luego implementar el movimiento Semántica.

int main()
{
     
     
	hdm::string s1("hello world");
	// 这里s1是左值,调用的是拷贝构造
	hdm::string s2(s1);
	// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
	// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
	// 资源被转移给了s3,s1被置空了。
	hdm::string s3(std::move(s1));
	return 0;
}
imagen-20230828202828424

La función de interfaz de inserción de contenedores STL también agrega una versión de referencia de rvalue:
documento de lista STL

int main()
{
     
     
	list<hdm::string> lt;
	hdm::string s1("1111");
	// 这里调用的是拷贝构造
	lt.push_back(s1);
	// 下面调用都是移动构造
	lt.push_back("2222");
	lt.push_back(std::move(s1));
	return 0;
}

imagen-20230828203806122

4.5 Reenvío perfecto

&& referencia universal en plantillas

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

Para ser correcto: el contenido de salida debería ser el contenido escrito en el comentario anterior, pero en realidad no lo es. La razón es que el objeto pasado a t durante el proceso de paso de parámetros se ha convertido en un valor l cuando se llama posteriormente a Fun, porque se pasa al objeto t, después de que t recibe el valor, se guarda en el valor t y todo t se ha convertido en un valor l

imagen-20230828204238852

Solución: utilice std::forward, reenvío perfecto para conservar los atributos de tipo nativo del objeto durante el proceso de transferencia de parámetros.

template<typename T>
void PerfectForward(T&& t)
{
     
     
   // std::forward<T>(t)在传参的过程中保持了t的原生类型属性
	Fun(std::forward<T>(t));
}
imagen-20230828204826875

Escenarios de uso reales de reenvío perfecto:

template<class T>
struct ListNode
{
     
     
	ListNode* _next = nullptr;
	ListNode* _prev = nullptr;
	T _data;
};
template<class T>
class List
{
     
     
	typedef ListNode<T> Node;
public:
	List()
	{
     
     
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}
	void PushBack(T&& x)
	{
     
     
		//Insert(_head, x);
		Insert(_head, std::forward<T>(x));
	}
	void PushFront(T&& x)
	{
     
     
		//Insert(_head->_next, x);
		Insert(_head->_next, std::forward<T>(x));
	}
	void Insert(Node* pos, T&& x)
	{
     
     
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = std::forward<T>(x); // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
	void Insert(Node* pos, const T& x)
	{
     
     
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = x; // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
private:
	Node* _head;
};
int main()
{
     
     
	List<hdm::string> lt;
	lt.PushBack("1111");
	lt.PushFront("2222");
	return 0;
}
//运行结果
//string operator=(string&& s)---移动赋值
//string operator=(string&& s)-- - 移动赋值

5 nuevas funciones de clase

Funciones miembro predeterminadas
Hay 6 funciones miembro predeterminadas en la clase C++ original:

  1. Constructor

  2. incinerador de basuras

  3. constructor de copias

  4. sobrecarga de tareas de copia

  5. Obtener recarga de dirección

  6. const toma la dirección y sobrecarga,
    los últimos cuatro que son importantes son los primeros cuatro, y los dos últimos son de poca utilidad. La función miembro predeterminada es una función predeterminada que el compilador generará si no la escribimos. C++ 11 agrega dos nuevos: mover el constructor y mover la sobrecarga del operador de asignación.

Hay algunos puntos a tener en cuenta sobre la sobrecarga de constructores de movimientos y operadores de asignación de movimientos, como se muestra a continuación:

  • Si no implementa el constructor de movimientos usted mismo y no implementa ninguno de los destructores, copie la construcción y copie la sobrecarga de asignaciones. Luego, el compilador generará automáticamente un constructor de movimientos predeterminado. El constructor de movimientos generado de forma predeterminada realizará la copia miembro por miembro, byte por byte para los miembros de tipo integrado. Para los miembros de tipo personalizado, debe ver si el miembro implementa la construcción de movimientos. Si lo hace, el constructor de movimientos será llamado De lo contrario, se llamará al constructor de copia.

  • Si no implementa la función de sobrecarga de asignación de movimiento usted mismo y no implementa ninguna sobrecarga de destructor, construcción de copia y asignación de copia, el compilador generará automáticamente una asignación de movimiento predeterminada. El constructor de movimientos generado de forma predeterminada realizará la copia miembro por miembro, byte por byte para los miembros de tipo integrado. Para los miembros de tipo personalizado, debe ver si el miembro implementa la asignación de movimiento. Si lo hace, llamará mover la asignación. Si no lo hace, llamará a copiar la asignación. (La asignación de movimiento predeterminada es completamente similar a la construcción de movimiento anterior)

  • Si proporciona construcción de movimiento o asignación de movimiento, el compilador no proporcionará automáticamente construcción de copia ni asignación de copia.

// 以下代码在vs2013中不能体现,在vs2019下才能演示体现上面的特性。
class Person
{
     
     
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{
     
     }
//-----------------------------------//
	//如果没有自己实现下面任意一个,那么编译器就会帮我们默认生成一个移动构造和移动赋值
	/*Person(const Person& p)
	 :_name(p._name)
	,_age(p._age)
	{}*/
	/*Person& operator=(const Person& p)
	{
	if(this != &p)
	{
		_name = p._name;
		_age = p._age;
	}
		return *this;
	}*/
	/*~Person()
	{}*/
//-----------------------------------//
private:
	hdm::string _name;
	int _age;
};
int main()
{
     
     
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	Person s4;
	s4 = std::move(s2);
	return 0;
}
//运行结果:
//string(const string& s)---深拷贝
//string(string&& s)---移动构造
//string operator=(string&& s)---移动赋值

La palabra clave default que fuerza la generación de una función predeterminada:

C++ 11 le brinda más control sobre qué funciones predeterminadas se utilizan. Supongamos que desea utilizar una función predeterminada, pero por alguna razón esta función no se genera de forma predeterminada. Por ejemplo: si proporcionamos un constructor de copia, el constructor de movimiento no se generará, entonces podemos usar la palabra clave predeterminada para mostrar la generación del constructor de movimiento especificado.

class Person
{
     
     
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{
     
     }
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{
     
     }
	Person(Person&& p) = default;
private:
	hdm::string _name;
	int _age;
};
int main()
{
     
     
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);//使用了默认生成的移动构造
	return 0;
}
//运行结果
//string(const string& s)-- - 深拷贝
//string(string && s)-- - 移动构造

Deshabilite la eliminación de palabras clave para generar funciones predeterminadas:

Si desea limitar la generación de ciertas funciones predeterminadas, en C++ 98, configure la función como privada y solo declarela indefinida, de modo que se informará un error siempre que otros quieran llamarla. Es aún más simple en C ++ 11, simplemente agregue = eliminar a la declaración de función. Esta sintaxis le indica al compilador que no genere una versión predeterminada de la función correspondiente, y la función modificada con = eliminar se llama función de eliminación.

class Person
{
     
     
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{
     
     }
	Person(const Person& p) = delete;
private:
	hdm::string _name;
	int _age;
};
int main()
{
     
     
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}
//运行报错
//error C2280: “Person::Person(const Person &)”: 尝试引用已删除的函数

6. Plantillas de parámetros variables

La nueva característica de C++ 11 son las plantillas de parámetros variables, que le permiten crear plantillas de funciones y plantillas de clases que pueden aceptar parámetros variables. En comparación con C++ 98/03, las plantillas de clases y plantillas de funciones solo pueden contener un número fijo de parámetros de plantilla., Los parámetros de plantilla variables son sin duda una gran mejora. Sin embargo, debido a que los parámetros de plantilla variables son relativamente abstractos y su uso requiere ciertas habilidades, este conocimiento aún es relativamente oscuro. En esta etapa, es suficiente dominar algunas características básicas de la plantilla de parámetros variables. Nos detendremos aquí. Si es necesario, puede aprender más en profundidad.

La siguiente es una plantilla básica de función de parámetro variable.

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{
     
     }

Los argumentos del parámetro anterior tienen puntos suspensivos delante, por lo que es un parámetro de plantilla variable. Llamamos al parámetro con una elipse un "paquete de parámetros", que contiene de 0 a N (N>= 0) parámetros de plantilla. No podemos obtener directamente cada parámetro en el paquete de parámetros args. Solo podemos obtener cada parámetro en el paquete de parámetros expandiendo el paquete de parámetros. Esta es una característica principal del uso de parámetros de plantilla variables, y también es la mayor dificultad, es decir, cómo para expandir los parámetros variados de la plantilla. Dado que la sintaxis no admite el uso de args [i] para obtener parámetros variables, utilizamos algunos trucos extraños para obtener los valores del paquete de parámetros uno por uno.

Ampliación del paquete de parámetros en modo de función recursiva

template<class T>
void showList(T val)
{
     
     
	cout << val << endl;
}

template<class T,class ...Args>
void showList(T value, Args... args)
{
     
     
	cout << value << " ";
	showList(args...);
}

int main()
{
     
     
	showList(1);
	showList(1,'a');
	showList(1,'a',"string");
	return 0;
}

paquete de parámetros de expansión de expresión de coma

Este método de expandir el paquete de parámetros no requiere una función de terminación recursiva, sino que se expande directamente en el cuerpo de la función de expansión. printarg no es una función de terminación recursiva, sino una función que procesa cada parámetro en el paquete de parámetros. La clave para esta expansión local de paquetes de parámetros es la expresión de coma. Sabemos que las expresiones de coma ejecutarán las expresiones que preceden a la coma en orden. La expresión de coma en la función de expansión: (printarg(args), 0) también sigue este orden de ejecución. Printarg(args) se ejecuta primero y luego el resultado de la expresión de coma es 0. Al mismo tiempo, se utiliza otra característica de C++ 11: la lista de inicialización. Para inicializar una matriz de longitud variable a través de la lista de inicialización, {(printarg(args), 0)...} se expandirá a (( printarg(arg1),0 ), (printarg(arg2),0), (printarg(arg3),0), etc… ), eventualmente creará una matriz int arr[sizeof… (Args)] cuyos valores de elemento son todos 0. Dado que es una expresión de coma, en el proceso de creación de la matriz, la parte printarg (args) delante de la expresión de coma se ejecutará primero para
imprimir los parámetros, es decir, el paquete de parámetros se expandirá durante la construcción de la matriz int. El propósito de esta matriz es simplemente expandir el paquete de parámetros durante el proceso de construcción de la matriz.

template<class T>
void PrintArg(T value)
{
     
     
	cout << value << " ";
}

template<class ...Args>
void showList( Args... args)
{
     
     
	int arr[] = {
     
      (PrintArg(args),0)... };
	cout << endl;
}

int main()
{
     
     
	showList(1000);
	showList(1000,'a');
	showList(1000,'a',"string");
	return 0;
}

7. expresión lambda

7.1 Un ejemplo en C++98

En C++98, si desea ordenar los elementos de una colección de datos, puede utilizar el método std::sort.

#include <algorithm>
#include <functional>
void PrintArr(int arr[],int size){
     
     
	for (int i=0;i<size;++i){
     
     
		cout << arr[i] << " ";
	}
	cout << endl;
}
int main(){
     
     
	int arr[] = {
     
      3,2,1,5,6,4,7,8,0 };
	// 默认按照小于比较,排出来结果是升序
	std::sort(arr, arr + sizeof(arr) / sizeof(arr[0]));
	PrintArr(arr, sizeof(arr) / sizeof(arr[0]));

	// 如果需要降序,需要改变元素的比较规则
	std::sort(arr, arr + sizeof(arr) / sizeof(arr[0]), greater<int>());
	PrintArr(arr, sizeof(arr) / sizeof(arr[0]));
	return 0;
}
//运行结果
//0 1 2 3 4 5 6 7 8
//8 7 6 5 4 3 2 1 0

Si los elementos a ordenar son de un tipo personalizado, el usuario debe definir las reglas de comparación para la clasificación:

struct Goods{
     
     
	string _name; // 名字
	double _price; // 价格
	int _evaluate; // 评价
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{
     
     }
};
struct ComparePriceLess//比较方式的仿函数{
     
     
	bool operator()(const Goods& gl, const Goods& gr){
     
     
		return gl._price < gr._price;
	}
};
struct ComparePriceGreater{
     
     
	bool operator()(const Goods& gl, const Goods& gr){
     
     
		return gl._price > gr._price;
	}
};
int main(){
     
     
	vector<Goods> v = {
     
      {
     
      "苹果", 2.1, 5 }, {
     
      "香蕉", 3, 4 }, {
     
      "橙子", 2.2,3 }, {
     
      "菠萝", 1.5, 4 } };
	sort(v.begin(), v.end(), ComparePriceLess());
	sort(v.begin(), v.end(), ComparePriceGreater());
	return 0;
}

Con el desarrollo de la sintaxis de C++, la gente comenzó a sentir que el método de escritura anterior era demasiado complicado. Cada vez, para implementar un algoritmo, se tenía que escribir una nueva clase. Si la lógica de comparación era diferente cada vez, se tenían que crear varias clases. para ser implementado Especialmente el nombramiento de la misma clase trae grandes inconvenientes a los programadores. Por lo tanto, las expresiones Lambda aparecieron en la sintaxis de C++11 .

7.2 expresión lambda

demostración

int main(){
     
     
	vector<Goods> v = {
     
      {
     
      "苹果", 2.1, 5 }, {
     
      "香蕉", 3, 4 }, {
     
      "橙子", 2.2,3 }, {
     
      "菠萝", 1.5, 4 } };
	//根据价格排升序
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
     
     
		return g1._price < g2._price; });
	//根据价格排降序
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
     
     
		return g1._price > g2._price; });
	//根据评价排升序
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
     
     
		return g1._evaluate < g2._evaluate; });
	//根据评价排降序
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
     
     
		return g1._evaluate > g2._evaluate; });
	return 0;
}

El código anterior se resuelve usando la expresión lambda en C ++ 11. Se puede ver que la expresión lambda es en realidad una función anónima.

7.3 sintaxis de expresión lambda

Formato de escritura de expresión lambda: [lista de captura] (parámetros) mutable -> tipo de retorno {declaración}

  1. Descripción de cada parte de la expresión lambda.
  • [lista de captura]: Lista de captura . Esta lista siempre aparece al principio de la función lambda. El compilador usa [] para determinar si el siguiente código es una función lambda. La lista de captura puede capturar variables en el contexto para su uso por parte del función lambda .
  • (parámetros): lista de parámetros. De acuerdo con la lista de parámetros de una función ordinaria , si no es necesario pasar parámetros, puede omitirlo junto con ()
  • mutable: de forma predeterminada, una función lambda es siempre una función constante y mutable puede cancelar su constancia. Cuando se utiliza este modificador, la lista de parámetros no se puede omitir (incluso si el parámetro está vacío).
  • ->returntype: tipo de valor de retorno. Declare el tipo de valor de retorno de la función en forma de tipo de retorno de seguimiento. Esta parte se puede omitir cuando no hay valor de retorno. Si el tipo de valor de retorno es claro, también se puede omitir y el compilador deducirá el tipo de retorno .
  • {declaración}: cuerpo de la función. Dentro del cuerpo de la función, además de sus parámetros, están disponibles todas las variables capturadas.

Nota :
En la definición de la función lambda, la lista de parámetros y el tipo de valor de retorno son partes opcionales, y la lista de captura y el cuerpo de la función pueden estar vacíos. Entonces, la función lambda más simple en C++ 11 es: []{}; esta función lambda no puede hacer nada .

int main(){
     
     
	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[] {
     
     };
	// 省略参数列表和返回值类型,返回值类型由编译器推导为int
	int a = 3, b = 4;
	[=] {
     
     return a + 3; };
	// 省略了返回值类型,无返回值类型
	auto fun1 = [&](int c) {
     
     b = a + c; };
	fun1(10);
	cout << a << " " << b << endl;
	// 相对完善的lambda函数
	auto fun2 = [=, &b](int c)->int {
     
     return b += a + c; };
	cout << fun2(10) << endl;
	// 复制捕捉x
	int x = 10;
	auto add_x = [x](int a) mutable {
     
      x *= 2; return a + x; };
	cout << add_x(10) << endl;
	return 0;
}
  1. Descripción de la lista de captura

La lista de captura describe qué datos del contexto puede utilizar lambda y si se pasan por valor o por referencia .

  • [var]: Indica el método de transferencia de valor para capturar la variable var
  • [=]: Indica que el método de paso de valor captura todas las variables en el ámbito principal (incluida esta)
  • [&var]: Indica que la variable de captura var se pasa por referencia
  • [&]: Indica que la transferencia de referencia captura todas las variables en el ámbito principal (incluido este)
  • [this]: indica que el método de transferencia de valor captura el puntero this actual

Aviso:

  • a. El alcance principal se refiere al bloque de instrucciones que contiene la función lambda.

  • b. Sintácticamente, la lista de captura puede constar de varios elementos de captura, separados por comas.

Por ejemplo: [=, &a, &b]: captura las variables a y b por referencia, y captura todas las demás variables por valor. [&, a, this]: captura las variables a y this por valor, y captura otras variables por referencia
.

  • La lista de captura de c no permite que se pasen variables repetidamente; de ​​lo contrario, se producirán errores de compilación.

Por ejemplo: [=, a]: = ha capturado todas las variables mediante transferencia de valor y captura repetidamente

  • dLa lista de captura de funciones lambda fuera del alcance del bloque debe estar vacía.

  • La función lambda de e en el alcance del bloque solo puede capturar variables locales en el alcance principal. La captura de cualquier variable que no esté en el alcance o
    no local resultará en un error de compilación.

  • Las expresiones Lambda no se pueden asignar entre sí, incluso si parecen ser del mismo tipo.

void (*PF)();
int main(){
     
     
	auto f1 = [] {
     
     cout << "hello world" << endl; };
	auto f2 = [] {
     
     cout << "hello world" << endl; };
	//f1 = f2; // 编译失败--->提示找不到operator=()
	// 由于 Lambda 表达式是匿名的,因此不能直接将一个 Lambda 对象赋值给另一个 Lambda 对象。这是因为 Lambda 表达式没有默认的拷贝构造函数或赋值运算符重载,因此无法像普通的对象一样进行拷贝或赋值。
	// 允许使用一个lambda表达式拷贝构造一个新的副本
	auto f3(f2);
	f3();
	// 可以将lambda表达式赋值给相同类型的函数指针
	PF = f2;
	PF();
	return 0;
}

7.4 Objetos de función y expresiones lambda

El objeto función, también conocido como funtor, es un objeto que se puede utilizar como una función, es un objeto de clase que sobrecarga el operador operador () en la clase.

class Rate{
     
     
public:
	Rate(double rate) : _rate(rate){
     
     }
	double operator()(double money, int year){
     
     
		return money * _rate * year;
	}
private:
	double _rate;
};
int main(){
     
     
	// 函数对象
	double rate = 0.5;
	Rate r1(rate);
	cout << r1(10000, 2) << endl;
	// lamber
	auto r2 = [=](double monty, int year)->double {
     
     return monty * rate * year;};
	cout << r2(10000, 2) << endl;
	return 0;
}
//运行结果
//10000
//10000

En términos de uso, los objetos de función son exactamente iguales a las expresiones lambda.

El objeto de función tiene la tasa como variable miembro, y el valor inicial se puede dar al definir el objeto, y la expresión lambda puede capturar directamente la variable a través de la lista de captura.

imagen-20230830213721439

De hecho, la forma en que el compilador subyacente maneja las expresiones lambda es completamente similar a la de los objetos de función, es decir, si se define una expresión lambda, el compilador generará automáticamente una clase en la que operador ().

8. Envoltorio

Envoltorios de funciones
Los envoltorios de funciones también se denominan adaptadores. La función en C++ es esencialmente una plantilla de clase y un contenedor.
Entonces, echemos un vistazo, ¿por qué necesitamos funciones?

//ret = func(x);
// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
//是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
//为什么呢?我们继续往下看
template<class F, class T>
T useF(F f, T x){
     
     
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i){
     
     
	return i / 2;
}
struct Functor{
     
     
	double operator()(double d){
     
     
		return d / 3;
	}
};
int main(){
     
     
	// 函数名
	cout << useF(f, 11.11) << endl;
	// 函数对象
	cout << useF(Functor(), 11.11) << endl;
	// lamber表达式
	cout << useF([](double d)->double {
     
      return d / 4; }, 11.11) << endl;
	return 0;
}
imagen-20230830223825349

A través de la verificación del programa anterior, encontraremos que se crean tres instancias de la plantilla de función useF.

Wrapper puede resolver muy bien los problemas anteriores.

std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
// 使用方法如下:
#include <functional>
int f(int a, int b){
     
     
	return a + b;
}
struct Functor{
     
     
public:
	int operator() (int a, int b){
     
     
		return a + b;
	}
};
class Plus{
     
     
public:
	static int plusi(int a, int b){
     
     
		return a + b;
	}
	double plusd(double a, double b){
     
     
		return a + b;
	}
};
using func_t = std::function<int(int, int)>;
int main(){
     
     
	// 函数名(函数指针)
	func_t func1 = f;
	cout << func1(1, 2) << endl;
	// 函数对象
	func_t func2 = Functor();
	cout << func2(1, 2) << endl;
	// lamber表达式
	func_t func3 = [](const int a, const int b)
	{
     
     return a + b; };
	cout << func3(1, 2) << endl;
	// 类的成员函数
	func_t func4 = &Plus::plusi;
	cout << func4(1, 2) << endl;
	std::function<double(Plus, double, double)> func5 = &Plus::plusd;
	cout << func5(Plus(), 1.1, 2.2) << endl;
	return 0;
}
//运行结果
//3
//3
//3
//3
//3.3

Con el contenedor, ¿cómo resolver el problema de la baja eficiencia de la plantilla y las múltiples instancias?

#include <functional>
template<class F, class T>
T useF(F f, T x){
     
     
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i){
     
     
	return i / 2;
}
struct Functor{
     
     
	double operator()(double d){
     
     
		return d / 3;
	}
};
int main(){
     
     
	// 函数名
	std::function<double(double)> func1 = f;
	cout << useF(func1, 11.11) << endl;
	// 函数对象
	std::function<double(double)> func2 = Functor();
	cout << useF(func2, 11.11) << endl;
	// lamber表达式
	std::function<double(double)> func3 = [](double d)->double {
     
      return d /
		4; };
	cout << useF(func3, 11.11) << endl;
	return 0;
}

imagen-20230830224455323

unir

La función std::bind se define en el archivo de encabezado y es una plantilla de función. Es como un contenedor de funciones (adaptador), que acepta un objeto invocable y genera un nuevo objeto invocable para "adaptar" el objeto original. En términos generales, podemos usarlo para convertir una función fn que originalmente recibió N parámetros y, al vincular algunos parámetros, devolver una nueva función que recibe M (M puede ser mayor que N, pero esto no tiene sentido) parámetros. Al mismo tiempo, el uso de la función std::bind también puede implementar operaciones como el ajuste del orden de los parámetros.

// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

La función de vinculación puede considerarse como un adaptador de función general que acepta un objeto invocable y genera un nuevo objeto invocable para "adaptar" la lista de parámetros del objeto original.
La forma general de llamar a bind: auto newCallable = bind(callable,arg_list); donde newCallable en sí es un objeto invocable y arg_list es una lista de parámetros separados por comas correspondientes a los parámetros del invocable dado. Cuando llamamos a newCallable, newCallable llamará a invocable y le pasará los parámetros en arg_list.
Los parámetros en arg_list pueden contener nombres de la forma _n, donde n es un número entero. Estos parámetros son "marcadores de posición" que representan los parámetros de newCallable. Ocupan la "posición" de los parámetros pasados ​​a newCallable. El valor n indica la posición del parámetro en el objeto invocable generado: _1 es el primer parámetro de newCallable, _2 es ​​el segundo parámetro, y así sucesivamente.

void Print(const char* str, int value)
{
     
     
	cout << str << value << endl;
}
int main()
{
     
     
	const char* str = "bind---";
	int value = 1;
	Print(str, value);//正常用法
	auto func1 = std::bind(Print, str, std::placeholders::_1);//绑定一个参数
	func1(value + 1);
	auto func2 = std::bind(Print, str, value+2);//绑定两个参数
	func2();
	return 0;
}
//运行结果
//bind-- - 1
//bind-- - 2
//bind-- - 3

9. Biblioteca de hilos

9.1 Una breve introducción a la clase de hilo.

Antes de C++ 11, los problemas de subprocesos múltiples estaban relacionados con la plataforma. Por ejemplo, Windows y Linux tenían cada uno sus propias interfaces, lo que hacía que el código fuera menos portátil. La característica más importante de C++ 11 es la compatibilidad con subprocesos, lo que hace que C++ ya no necesite depender de bibliotecas de terceros para la programación paralela y también introduce el concepto de clases atómicas en operaciones atómicas. Para utilizar subprocesos de la biblioteca estándar, se debe incluir el archivo de encabezado <thread>.

Clases de subprocesos C ++ 11

Nombre de la función Función
hilo() Construya un objeto de hilo, no asociado con ninguna función de hilo, es decir, no se inicia ningún hilo
hilo(fn, args1, args2,…) Construya un objeto de subproceso y asocie la función de subproceso fn, args1, args2, ... como parámetros de la función de subproceso
obtener_id() Obtener ID del hilo
jionable() Si el subproceso todavía se está ejecutando, unible representa un subproceso en ejecución.
jion() Después de llamar a la función, el hilo se bloqueará y cuando el hilo finalice, el hilo principal continuará ejecutándose.
despegar() Se llama inmediatamente después de que se crea el objeto de hilo y se utiliza para separar el hilo creado del objeto de hilo. El hilo separado se convierte en un hilo de fondo. La "vida o muerte" del hilo creado no tiene nada que ver con el hilo principal.

Aviso:

  1. Un hilo es un concepto en el sistema operativo: un objeto de hilo se puede asociar con un hilo para controlar el hilo y obtener el estado del hilo.

  2. Cuando se crea un objeto de subproceso, no se proporciona ninguna función de subproceso y el objeto en realidad no corresponde a ningún subproceso.

#include <thread>
int main(){
     
     
	std::thread t1;
	cout << t1.get_id() << endl;
	return 0;
}
//运行结果
//0

El tipo de valor de retorno de get_id() es el tipo de identificación, y el tipo de identificación es en realidad una clase encapsulada en el espacio de nombres std::thread, que contiene una estructura:

// vs下查看
typedef struct
{
     
      /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;
  1. Cuando se crea un objeto de subproceso y se asocia una función de subproceso con el subproceso, el subproceso se inicia y se ejecuta junto con el subproceso principal.
    Las funciones de subproceso generalmente se pueden proporcionar de las tres formas siguientes:
  • puntero de función

  • expresión lambda

  • objeto de función

#include <thread>
#include <windows.h>
void ThreadFun(int value){
     
     
	cout << "thread" << value << endl;
}

class TF{
     
     
public:
	void operator()(int value){
     
     
		cout << "thread" << value << endl;
	}
};

int main(){
     
     
	std::thread t1(ThreadFun, 1);
	Sleep(1);
	TF tf;
	std::thread t2(tf, 2);
	Sleep(1);
	std::thread t3([](int value) {
     
     cout << "thread" << value << endl; }, 3);
	t1.join();
	t2.join();
	t3.join();
	cout << "Main thread!" << endl;
	return 0;
}
//运行结果
//thread1
//thread2
//thread3
//Main thread!
  1. La clase de subproceso es a prueba de copia y no permite la construcción y asignación de copias, pero sí la construcción y asignación de movimientos, es decir, el estado de un subproceso asociado con un objeto de subproceso se transfiere a otros objetos de subproceso. El hilo no está previsto durante la transferencia.

  2. Puede utilizar la función jionable() para determinar si el hilo es válido. Si se da alguna de las siguientes situaciones, el hilo no es válido.

  • Objeto de hilo construido usando un constructor sin argumentos

  • El estado del objeto de hilo se ha transferido a otros objetos de hilo.

  • El hilo ha terminado llamando a jion o detach.

9.2 Parámetros de la función de hilo

Los parámetros de la función del subproceso se copian en el espacio de la pila del subproceso en forma de copia de valor . Por lo tanto, incluso si los parámetros del subproceso son tipos de referencia, los parámetros reales externos no se pueden modificar después de modificarlos en el subproceso, porque en realidad se refieren a la copia en la pila de subprocesos, en lugar de argumentos externos.

#include <thread>
void ThreadFunc1(int& x){
     
     
	x += 10;
}
void ThreadFunc2(int* x){
     
     
	*x += 100;
}
int main(){
     
     
	int a = 10;
	// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际
	//引用的是线程栈中的拷贝
	/*thread t1(ThreadFunc1, a);
	t1.join();
	cout << a << endl;*/
	//如果想要通过形参改变外部实参时,必须借助std::ref()函数,否则程序会报错(vs2019)
	thread t2(ThreadFunc1, std::ref(a));
	t2.join();
	cout << a << endl;
	// 地址的拷贝
	thread t3(ThreadFunc2, &a);
	t3.join();
	cout << a << endl;
	return 0;
}
//运行结果
//20
//120

Nota: Si una función miembro de clase se usa como parámetro de subproceso, debe usarse como parámetro de función de subproceso.

9.4 Biblioteca de operaciones atómicas (atómica)

El principal problema del subproceso múltiple es el problema causado por los datos compartidos (es decir, la seguridad de los subprocesos) . Si todos los datos compartidos son de solo lectura, entonces no hay problema, porque la operación de solo lectura no afectará los datos, y mucho menos los modificará, por lo que todos los subprocesos obtendrán los mismos datos. Sin embargo, cuando uno o más subprocesos quieren modificar datos compartidos, surgen muchos problemas potenciales . Por ejemplo:

int sum = 0;
void fun(int size){
     
     
	for (int i = 0; i < size; ++i)
	{
     
     
		sum++;
	}
}
int  main(){
     
     
	cout << "运行之前的sum=" << sum << endl;
   //分别让两个线程同时对同一个变量sum++
	thread t1(fun,100000);
	thread t2(fun,100000);
	t1.join();
	t2.join();
	cout << "运行之后的sum=" << sum << endl;
	return 0;
}
//运行结果:sum运行之后的值<=200000

La solución tradicional en C++98: los datos modificados compartidos se pueden bloquear y proteger

#include <mutex>
std::mutex mt;//创建一把锁
//分别让两个线程同时对同一个变量sum++
int sum = 0;
void fun(int size){
     
     
	for (int i = 0; i < size; ++i){
     
     
		mt.lock();
		sum++;
		mt.unlock();
	}
}

int  main(){
     
     
	
	cout << "运行之前的sum=" << sum << endl;
	thread t1(fun, 100000);
	thread t2(fun, 100000);
	t1.join();
	t2.join();
	cout << "运行之后的sum=" << sum << endl;
	return 0;
}
//运行结果
//运行之前的sum = 0
//运行之后的sum = 200000

Aunque el bloqueo se puede resolver, una desventaja del bloqueo es que mientras un subproceso esté tratando con suma ++, otros subprocesos se bloquearán, lo que afectará la eficiencia de la operación del programa. Además, si el bloqueo no está bien controlado, puede fácilmente provocar un punto muerto.

Por lo tanto, las operaciones atómicas se introdujeron en C++ 11. La llamada operación atómica: es decir, una o una serie de operaciones que no se pueden interrumpir.El tipo de operación atómica introducido por C ++ 11 hace que la sincronización de datos entre subprocesos sea muy eficiente.

imagen-20230901152602183

Nota: Cuando necesite utilizar las variables de operación atómica anteriores, debe agregar un archivo de encabezado

#include <atomic>
atomic_int sum = 0;
void fun(int size){
     
     
	for (int i = 0; i < size; ++i){
     
     
		sum++;// 原子操作
	}
}

int  main(){
     
     

	cout << "运行之前的sum=" << sum << endl;
	thread t1(fun, 100000);
	thread t2(fun, 100000);
	t1.join();
	t2.join();
	cout << "运行之后的sum=" << sum << endl;
	return 0;
}
//运行结果
//运行之前的sum = 0
//运行之后的sum = 200000

En C ++ 11, los programadores no necesitan bloquear y desbloquear variables de tipo atómico, y los subprocesos pueden tener acceso mutuamente exclusivo a variables de tipo atómico . De manera más general, los programadores pueden usar la plantilla de clase atómica para definir cualquier tipo atómico necesario.

atomic<T> t; // 声明一个类型为T的原子类型变量t

Nota: Los tipos atómicos generalmente pertenecen a datos de "recursos" y varios subprocesos solo pueden acceder a una copia de un solo tipo atómico. Por lo tanto, en C ++ 11, los tipos atómicos solo se pueden construir a partir de sus parámetros de plantilla, y los tipos atómicos no permitido copiar Construcción, mover construcción, operador =, etc. Para evitar accidentes , la biblioteca estándar ha eliminado la sobrecarga de copiar construcción, mover construcción y operador de asignación en la clase de plantilla atómica de forma predeterminada .

#include <atomic>
int main()
{
     
     
	atomic<int> a1(0);
	//atomic<int> a2(a1); // 编译失败,尝试引用已删除的函数
	atomic<int> a2(0);
	//a2 = a1; // 编译失败,尝试引用已删除的函数
	return 0;
}

9.5 lock_guard 与unique_lock

En un entorno de subprocesos múltiples, si desea garantizar la seguridad de una determinada variable, solo necesita configurarla en el tipo atómico correspondiente, que es eficiente y no propenso a problemas de interbloqueo. Pero en algunos casos, es posible que necesitemos garantizar la seguridad de un fragmento de código, por lo que solo podemos controlarlo mediante bloqueos.
Por ejemplo: un hilo suma 100 veces a la variable número y el otro resta 100 veces. Después de cada operación suma 1 o resta 1, se genera el resultado del número. Requisito: el valor final del número es 0.

#include <thread>
#include <mutex>
int number = 0;
mutex g_lock;
int ThreadProc1(){
     
     
	for (int i = 0; i < 100; i++){
     
     
		g_lock.lock();
		++number;
		cout << "thread 1 :" << number << endl;
		g_lock.unlock();
	}
	return 0;
}
int ThreadProc2(){
     
     
	for (int i = 0; i < 100; i++){
     
     
		g_lock.lock();
		--number;
		cout << "thread 2 :" << number << endl;
		g_lock.unlock();
	}
	return 0;
}
int main(){
     
     
	thread t1(ThreadProc1);
	thread t2(ThreadProc2);
	t1.join();
	t2.join();
	cout << "number:" << number << endl;
	return 0;
}

Defectos del código anterior: cuando el control de bloqueo no es bueno, puede ocurrir un punto muerto . Los más comunes son el código que regresa en medio del bloqueo o se lanzan excepciones dentro del alcance del bloqueo . Por lo tanto: C ++ 11 usa RAII para encapsular bloqueos, es decir, lock_guard y Unique_lock.

int ThreadProc1()
{
     
     
	for (int i = 0; i < 100; i++)
	{
     
     
		//lock_guard<mutex> lock(g_lock);
       unique_lock<mutex> lock(g_lock);
		++number;
		cout << "thread 1 :" << number << endl;
	}
	return 0;
}
int ThreadProc2()
{
     
     
	for (int i = 0; i < 100; i++)
	{
     
     
		//lock_guard<mutex> lock(g_lock);
		unique_lock<mutex> lock(g_lock);
		--number;
		cout << "thread 2 :" << number << endl;
	}
	return 0;
}

9.5.1 Tipos de exclusión mutua

9.5.1 Tipos de exclusión mutua

  1. std::mutex
    es el mutex más básico proporcionado por C++ 11. Los objetos de esta clase no se pueden copiar ni mover. Las tres funciones de mutex más utilizadas
    :
Nombre de la función función función
cerrar con llave() Bloquear: bloquear el mutex
desbloquear() Desbloquear: Liberar la propiedad del mutex
intentar_lock() Intente bloquear el mutex. Si el mutex está ocupado por otro hilo, el hilo actual no se bloqueará.

Tenga en cuenta que cuando la función de subproceso llama a lock(), pueden ocurrir las siguientes tres situaciones:

  • Si el mutex no está bloqueado actualmente, el subproceso que realiza la llamada bloquea el mutex y lo mantiene hasta que se llama a desbloquear.

  • Si el mutex actual está bloqueado por otro subproceso, el subproceso que llama actualmente está bloqueado

  • Si el mutex actual está bloqueado por el hilo de llamada actual, se producirá un punto muerto

Cuando una función de subproceso llama a try_lock(), pueden ocurrir las siguientes tres situaciones:

  • Si el mutex actual no está ocupado por otros subprocesos, el subproceso bloquea el mutex hasta que el subproceso llama a desbloquear para liberar el mutex.

  • Si el mutex actual está bloqueado por otros subprocesos, el subproceso que llama actualmente devuelve falso y no será bloqueado

  1. std::recursive_mutex (mutex recursivo)

    Permite que el mismo subproceso bloquee el mutex varias veces (es decir, lo bloquee recursivamente) para obtener múltiples capas de propiedad del objeto mutex. Al liberar el mutex, debe llamar a unlock() la misma cantidad de veces que la jerarquía de bloqueo. de lo contrario, std::recursive_mutex tiene aproximadamente las mismas características que std::mutex.

  2. std::timed_mutex

    Hay dos funciones miembro más que std::mutex, try_lock_for() y try_lock_until().

    • try_lock_for()
      acepta un rango de tiempo, lo que significa que el hilo se bloqueará si no adquiere el bloqueo dentro de este período de tiempo (a diferencia de try_lock() de std::mutex, try_lock devuelve falso directamente si no se adquiere el bloqueo cuando se llama), si otros subprocesos liberan el bloqueo durante este período, el subproceso puede adquirir el bloqueo en el mutex, y si se agota el tiempo de espera (es decir, el bloqueo no se adquiere dentro del tiempo especificado), devuelve falso.

    • try_lock_until()
      acepta un punto de tiempo como parámetro. Si el hilo no adquiere el bloqueo antes de que llegue el punto de tiempo especificado, será bloqueado. Si otros hilos liberan el bloqueo durante este período, el hilo puede adquirir el bloqueo en el mutex. Si se agota el tiempo de espera (es decir, el bloqueo no se obtiene dentro del tiempo especificado), se devuelve falso.

  3. std::recursive_timed_mutex

9.5.2 bloqueo_guardia

std::lock_gurad es una clase de plantilla definida en C++11. La definición es la siguiente:

template<class _Mutex>
class lock_guard
{
     
     
public:
	// 在构造lock_gard时,_Mtx还没有被上锁
	explicit lock_guard(_Mutex& _Mtx)
		: _MyMutex(_Mtx)
	{
     
     
		_MyMutex.lock();
	}
	// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
	lock_guard(_Mutex& _Mtx, adopt_lock_t)
		: _MyMutex(_Mtx)
	{
     
     }
	~lock_guard() _NOEXCEPT
	{
     
     
		_MyMutex.unlock();
	}
	lock_guard(const lock_guard&) = delete;
	lock_guard& operator=(const lock_guard&) = delete;
private:
	_Mutex& _MyMutex;
};

Como se puede ver en el código anterior, la plantilla de clase lock_guard encapsula principalmente el mutex que administra a través de RAII. Cuando se requiere bloqueo, solo necesita crear una instancia de un lock_guard con cualquier mutex introducido anteriormente .
La desventaja de lock_guard es que es demasiado simple y los usuarios no tienen forma de controlar el bloqueo , por lo que C++ 11 proporciona Unique_lock.

9.5.3 bloqueo_único

Similar a lock_gard, la plantilla de clase Unique_lock también usa RAII para encapsular bloqueos y también administra las operaciones de bloqueo y desbloqueo de objetos mutex de manera exclusiva, es decir, no se pueden realizar copias entre sus objetos . Durante la construcción (o asignación de movimiento), el objeto Unique_lock necesita pasar un objeto Mutex como parámetro, y el objeto Unique_lock recién creado es responsable de las operaciones de bloqueo y desbloqueo del objeto Mutex entrante. Cuando se utiliza el tipo de mutex anterior para crear una instancia de un objeto Unique_lock, se llama automáticamente al constructor para bloquearlo. Cuando se destruye el objeto Unique_lock, se llama automáticamente al destructor para desbloquearlo, lo que puede evitar fácilmente problemas de interbloqueo .

A diferencia de lock_guard, Unique_lock es más flexible y proporciona más funciones para miembros:

  • Operaciones de bloqueo/desbloqueo : bloquear, try_lock, try_lock_for, try_lock_until y desbloquear

  • Operaciones de modificación : mover asignación, intercambiar (intercambiar: intercambiar la propiedad del mutex administrado por otro objeto de bloqueo único), liberar (liberar: devolver un puntero al objeto mutex que administra y liberar la propiedad)

  • Obtener atributos : owns_lock (devuelve si el objeto actual está bloqueado), operador bool() (misma función que owns_lock()), mutex (devuelve el puntero del mutex administrado por el Unique_lock actual).

9.6 Variables de condición

Documentación para variables de condición.

Esta sección demuestra principalmente el uso de condition_variable

Ejemplo: admite dos subprocesos para imprimir alternativamente, uno imprime números impares y el otro imprime números pares

#include <iostream>
#include <thread>
#include <condition_variable>
using namespace std;
int main(){
     
     
	mutex mtx;
	std::condition_variable con;//条件变量
	int i = 1;
	int flag = true;
	//打印奇数
	thread t1([&] {
     
     
		while (i < 100){
     
     
			unique_lock<mutex> lock(mtx);
			while (!flag)con.wait(lock);//这里必须是while,不能用if
			cout <<"t1----" <<this_thread::get_id()<<"-----" << i++ << endl;
			flag = false;
			con.notify_one();//通知另一个线程

		}
		});

	//打印偶数
	thread t2([&] {
     
     
		while (i<=100){
     
     
			unique_lock<mutex> lock(mtx);
			while(flag)con.wait(lock);
			cout << "t2----" << this_thread::get_id() << "-----" << i++ << endl;
			flag = true;
			con.notify_one();//通知另一个线程
		}
		});
	t1.join();
	t2.join();
	return 0;
}

Supongo que te gusta

Origin blog.csdn.net/dongming8886/article/details/132631283
Recomendado
Clasificación