[C++]——Nouvelles fonctionnalités C++11 "référence rvalue et sémantique de déplacement"

Avant-propos :

  • Dans ce numéro, nous présenterons des connaissances pertinentes sur les références rvalue C++. Chacun doit être capable de maîtriser le contenu des connaissances sur cette question, et c'est un objectif clé de l'inspection lors de l'entretien.

Table des matières

(1) référence lvalue et référence rvalue

1. Qu'est-ce qu'une lvalue ? Qu'est-ce qu'une référence lvalue ?

2. Qu'est-ce qu'une rvalue ? Qu'est-ce qu'une référence rvalue ?

(2) Comparaison entre la référence lvalue et la référence rvalue

(3) Scénarios d'utilisation et signification des références rvalue

(4) Transmission parfaite 

1. Concept

2. La référence universelle && dans le modèle

3、std::avant

Résumer


(1) référence lvalue et référence rvalue

La syntaxe C++ traditionnelle a une syntaxe de référence, et la nouvelle fonctionnalité de syntaxe de référence rvalue est ajoutée en C++11, donc désormais les références que nous avons apprises auparavant sont appelées références lvalue. Qu'il s'agisse d'une référence lvalue ou d'une référence rvalue, un alias est donné à l'objet.

1. Qu'est-ce qu'une lvalue ? Qu'est-ce qu'une référence lvalue ?

Il existe des références dans la norme C++98/03, qui sont représentées par "&" . Cependant, cette méthode de référence présente un défaut, c'est-à-dire que dans des circonstances normales, seules les lvalues ​​​​en C++ peuvent être utilisées et les références ne peuvent pas être ajoutées aux rvalues ​​. Par exemple:
 

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

	return 0;
}

La sortie montre :

 【expliquer】

  • Comme indiqué ci-dessus, le compilateur nous permet de créer une référence à la valeur num, mais pas à la valeur 10. Par conséquent, les références dans la norme C++98/03 sont également appelées références lvalue.
     

Alors, qu’est-ce qu’une lvalue exactement ? Qu'est-ce qu'une référence lvalue ?

  1. Une lvalue est une expression qui représente des données (comme un nom de variable ou un pointeur déréférencé). Nous pouvons obtenir son adresse + nous pouvons lui attribuer une valeur . Une lvalue peut apparaître sur le côté gauche du symbole d'affectation, et une rvalue ne peut pas apparaître sur le côté gauche du symbole d’affectation. ;
  2. La lvalue après le modificateur const, lorsqu'elle est définie, ne peut pas se voir attribuer une valeur, mais son adresse peut être prise. Une référence lvalue est une référence à une lvalue et un alias est donné à la lvalue.
     

Remarque : Bien que la norme C++98/03 ne prenne pas en charge l'établissement de références lvalue non const aux rvalues, elle autorise l'utilisation de références lvalue const pour opérer sur les rvalues. C'est-à-dire qu'une référence lvalue constante peut opérer à la fois sur les lvalues ​​​​et sur les rvalues, par exemple :
 

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'est-ce qu'une rvalue ? Qu'est-ce qu'une référence rvalue ?
 

Nous savons que les valeurs n'ont souvent pas de noms , elles ne peuvent donc être utilisées que par référence. Cela crée un problème. Dans le développement réel, nous devrons peut-être modifier la rvalue (requis lors de l'implémentation de la sémantique de déplacement). Évidemment, la méthode de référence lvalue ne fonctionne pas.


Pour cette raison, le standard C++11 introduit une autre méthode de référence, appelée référence rvalue, représentée par "&&" :

  • Une rvalue est également une expression représentant des données , telles que : une constante littérale, une valeur de retour d'expression, une valeur de retour de fonction (cela ne peut pas être un retour de référence lvalue), etc. ;
  • Une rvalue peut apparaître sur le côté droit d'un symbole d'affectation, mais ne peut pas apparaître sur le côté gauche d'un symbole d'affectation. Une rvalue ne peut pas prendre d'adresse ;
  • Une référence rvalue est une référence à une rvalue, donnant un alias à la 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;
}

La sortie montre :

 Mais s’il s’agit des expressions suivantes, une erreur se produira :

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

La sortie montre :

Il convient de noter que l'adresse d'une rvalue ne peut pas être obtenue , mais donner un alias à la rvalue entraînera le stockage de la rvalue dans un emplacement spécifique, et l'
adresse de cet emplacement peut être obtenue. Par exemple, le code suivant affiche :

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

La sortie montre :

 Lorsque nous ne voulons pas être modifiés, nous pouvons ajouter le mot-clé [const] :

【expliquer】

  1. L'adresse du littéral 10 ne peut pas être prise, mais une fois que rr1 est référencé, l'adresse de rr1 peut être prise, ou rr1 peut être modifié ;
  2. Si vous ne souhaitez pas que rr1 soit modifié, vous pouvez utiliser const int&& rr1 pour référencer ;
  3. Est-ce que cela semble incroyable ? Ce n'est pas le cas pour comprendre l'utilisation réelle des références rvalue, et cette fonctionnalité n'est pas importante.
     

(2) Comparaison entre la référence lvalue et la référence rvalue

Résumé de référence lvalue :

  • 1. Les références Lvalue ne peuvent faire référence qu’à des lvalues, pas à des rvalues.
  • 2. Mais les références const lvalue peuvent faire référence à la fois aux lvalues ​​​​et aux rvalues

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

La sortie montre :

 Un autre exemple est l'exemple suivant :

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

    return 0;
}

La sortie montre :

 Une référence lvalue ne peut faire référence qu’à une lvalue, pas à une rvalue. Mais lorsque nous ajoutons const, la référence lvalue peut alias la rvalue :

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

La sortie montre :

【expliquer】

Il convient de mentionner que bien que la syntaxe C++ prenne en charge la définition de références rvalue constantes, de telles références rvalue définies n'ont aucune utilité pratique :

const int& ra3 = 10;
  1. D'une part, les références rvalue sont principalement utilisées pour la sémantique de déplacement et le transfert parfait , où la première nécessite l'autorisation de modifier les rvalues ​​;
  2. Deuxièmement, la fonction d'une référence à rvalue constante est de faire référence à une rvalue non modifiable. Ce travail peut être complété par une référence à lvalue constante.
     


Résumé des références rvalue :

  • 1. Les références Rvalue ne peuvent faire référence qu’à des rvalues, pas à des lvalues.
  • 2. Mais les références rvalue peuvent déplacer les lvalues ​​suivantes.
     

Affichage des codes :

 Une erreur se produira lors du référencement d’une lvalue :


La conversion des lvalues ​​​​en références rvalue peut être prise en charge via le déplacement 

 En C++, moveun modèle de fonction qui convertit un objet donné en référence rvalue correspondante. Il n'effectue pas l'opération de déplacement de mémoire proprement dite, mais marque l'objet comme une valeur pouvant être déplacée. De cette manière, les utilisateurs peuvent exploiter ce balisage pour obtenir une sémantique de déplacement plus efficace.


(3) Scénarios d'utilisation et signification des références rvalue

Nous avons vu plus tôt que les références lvalue peuvent référencer à la fois les lvalues ​​et les rvalues, alors pourquoi C++11 propose-t-il également des références rvalue ? Est-ce superflu ? Jetons un coup d'œil aux défauts des références lvalue et comment les références rvalue peuvent compenser ces défauts !

Le code suivant existe : 

 【expliquer】

Premièrement, pour res1 et res2 dans le code ci-dessus, ils sont respectivement lvalue et rvalue ;

Alors réfléchissez-y, y a-t-il une différence entre copier des lvalues ​​​​et copier des rvalues ?

  • S'il s'agit d'un type intégré, la différence entre eux n'est pas très grande, mais pour un type personnalisé, la différence est très grande.
  • En raison de la valeur rvalue du type personnalisé, on l'appelle généralement une valeur mourante dans de nombreux endroits. Il s'agit généralement de la valeur de retour de certaines expressions, d'un appel de fonction, etc. ;
  • Pour les rvalues, il est divisé en prvalues ​​​​(en général, types intégrés) et will-values ​​(en général, types personnalisés)

Pour le res1 ci-dessus, en tant que lvalue, nous ne pouvons pas opérer dessus et ne pouvons effectuer qu'une copie approfondie. Parce que même si cela ressemble ici à une mission, il devrait en réalité s'agir d'une construction de copie ;

Quant à res2, c'est lui-même une rvalue. S'il s'agit d'un type personnalisé comme valeur mourante, nous n'avons pas besoin de le copier. À ce stade, le concept d’implémentation de référence rvalue déclenchant la construction est introduit.


Par exemple, il y a maintenant une chaîne que l'on simule en écrivant à la main :

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

Lorsque nous observons les res1 et res2 ci-dessus dans ce scénario :

 Nous pouvons constater que la rvalue ici est une copie profonde correspondante, ce qui entraînera évidemment un gaspillage inutile. Afin de résoudre les problèmes ci-dessus, nous pouvons introduire le concept de « construction de mouvements » :

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

Immédiatement après, en exécutant à nouveau le code ci-dessus, nous pouvons constater que le compilateur reconnaîtra automatiquement :

À ce stade, que pouvons-nous faire lorsque nous voulons simplement convertir s1 en rvalue ? C'est en fait très simple ( le déplacement est reflété ici ) :

 La sortie montre :

Nous pouvons également constater grâce au débogage que l’effet attendu est bien atteint à ce moment :

 【résumé】

De ce qui précède, nous pouvons constater que l'avantage des références lvalue est de réduire directement la copie

Les scénarios d'utilisation des références lvalue peuvent être divisés en deux parties suivantes :

  • Les paramètres et les valeurs de retour peuvent améliorer l'efficacité

Lacunes des références lvalue :

  • Mais lorsque l'objet renvoyé par la fonction est une variable locale, il n'existe pas en dehors de la portée de la fonction, il ne peut donc pas être renvoyé par référence lvalue, mais ne peut être renvoyé que par valeur.

Par exemple, nous avons maintenant le code suivant : 

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

【illustrer】

  • Comme vous pouvez le voir dans la fonction zp ::string to_string(int value), seul le retour par valeur peut être utilisé ici, et le retour par valeur provoquera au moins une construction de copie (s'il s'agit de compilateurs plus anciens, il peut s'agir de deux copies). constructions ) .

 Imprimons ensuite pour voir quel est le résultat :

 A cette époque, le coût du retour en valeur a été grandement résolu :

 Les références Rvalue et la sémantique de déplacement résolvent les problèmes ci-dessus :

  • Ajoutez une structure de déplacement à zp::string. L'essence de la structure de déplacement est de voler les ressources de la valeur du paramètre. Si l'espace est déjà là, alors il n'est pas nécessaire de faire une copie complète, on l'appelle donc une structure de déplacement, qui consiste à voler les ressources des autres pour se construire. ;

Ensuite, exécutez les deux appels de zp::to_string ci-dessus, nous constaterons que la construction de copie de la copie profonde n'est pas appelée ici, mais la construction de déplacement est appelée. Il n'y a pas de nouvel espace dans la construction de déplacement pour copier les données, donc l'efficacité est amélioré.


C++11 n'a pas seulement la construction de déplacement, mais aussi l'affectation de déplacement :

Ajoutez une fonction d'affectation de déplacement dans la classe zp::string, puis appelez zp::to_string(1234), mais cette fois l'objet rvalue renvoyé par zp::to_string(1234) est affecté à l'objet ret1, et le déplacement est appelée à cette époque structure.

La sortie montre :

 【expliquer】

  • Après avoir exécuté ici, nous voyons qu'une construction de déplacement et une affectation de déplacement sont appelées. Car s’il est reçu par un objet existant, le compilateur ne peut pas l’optimiser. La fonction zp::to_string utilisera d'abord la construction de génération str pour générer un objet temporaire, mais nous pouvons voir que le compilateur est suffisamment intelligent pour reconnaître str comme une valeur r et appeler la construction move. Attribuez ensuite cet objet temporaire comme valeur de retour de l'appel de fonction zp::to_string à ret1 et l'affectation de déplacement appelée ici.

(4) Transmission parfaite 

1. Concept

  • Le transfert parfait est une fonctionnalité introduite dans C++11, visant à permettre de transmettre avec précision les types de paramètres dans les modèles de fonctions ;
  • Il est principalement utilisé pour conserver la catégorie de valeur du paramètre réel transmis au modèle de fonction et la transmettre à la fonction appelée en interne, obtenant ainsi une préservation complète du type et de la catégorie de valeur ;

Par exemple:

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

【expliquer】

  1. Comme indiqué ci-dessus, la fonction Func() est appelée dans le modèle de fonction PerfectForward() ;
  2. Sur cette base, le transfert parfait fait référence à : si le paramètre t reçu par la fonction PerfectForward() est une lvalue, alors le paramètre t passé à Func() par cette fonction est également une lvalue ;
  3. En revanche, si le paramètre t reçu par la fonction function() est une rvalue, alors le paramètre t passé à la fonction Func() doit également être une rvalue.

En utilisant l’une ou l’autre forme de citation, le transfert peut être réalisé, mais la perfection n’est pas garantie. Par conséquent, si nous utilisons le langage C++ sous la norme C++ 98/03, nous pouvons utiliser la surcharge de modèles de fonctions pour obtenir un transfert parfait, par exemple :

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

La sortie montre :

 【expliquer】

  1. Dans l'exemple ci-dessus, nous avons défini deux modèles de fonctions surchargés  Func, l'un recevant les paramètres de référence lvalue T& arget l'autre recevant les paramètres de référence forward.T&& arg;
  2. Ensuite, nous définissons une fonction modèle PerfectForwarddont les paramètres sont également des références transmises T&& arg. À l'intérieur PerfectForwardde la fonction, nous Functraitons les paramètres passés en appelant la fonction ;
  3. Grâce au mécanisme de surcharge de fonctions, les paramètres lvalue transmis correspondront à la fonction qui reçoit la référence lvalue Func, et le paramètre rvalue transmis correspondra Funcà la fonction qui reçoit la référence transmise, afin qu'ils puissent être distingués et traités correctement ;
  4. Grâce à la surcharge des modèles de fonctions, nous pouvons distinguer les lvalues ​​​​et les rvalues ​​​​en fonction des types de paramètres et les traiter séparément, obtenant ainsi une correspondance et des opérations précises pour différentes catégories de valeurs.

2. La référence universelle && dans le modèle

Évidemment, l'utilisation mentionnée ci-dessus de fonctions de modèle surchargées pour obtenir un transfert parfait présente également des inconvénients. Cette méthode d'implémentation ne convient que dans les situations où la fonction de modèle n'a que quelques paramètres. Sinon, un grand nombre de modèles de fonctions surchargés devront être écrit, provoquant une redondance du code. Afin de permettre aux utilisateurs d'obtenir plus rapidement un transfert parfait, la norme C++11 permet l'utilisation de références rvalue dans les modèles de fonction pour obtenir un transfert parfait.

Toujours en prenant comme exemple la fonction PerfectForward(), pour obtenir un transfert parfait dans le standard C++11, il vous suffit d'écrire la fonction modèle suivante :

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

Prenons le code suivant comme exemple :

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

La sortie montre :

 【expliquer】

  1. Le && dans le modèle ne représente pas une référence rvalue, mais une référence universelle, qui peut recevoir à la fois des lvalues ​​​​et des rvalues.
  2. La référence universelle du modèle offre uniquement la possibilité de recevoir à la fois des références lvalue et des références rvalue.
  3. Cependant, la seule fonction des types référence est de limiter les types reçus, et ils dégénèrent en lvalues ​​lors des utilisations ultérieures.
  4. Si nous voulons pouvoir conserver ses attributs lvalue ou rvalue pendant le processus de transfert, nous devons utiliser le transfert parfait que nous apprendrons ci-dessous.
     

3、std::avant

Les développeurs du standard C++11 ont déjà pensé à une solution pour nous. Le nouveau standard introduit également une fonction modèle forword<T>(). Il suffit d'appeler cette fonction pour résoudre facilement ce problème.

  1. Le transfert parfait est généralement utilisé avec la référence de transfert et la fonction std::forward ;
  2. La référence transférée est un type de référence spécial, &&déclaré à l'aide de la syntaxe, utilisé pour capturer les paramètres réels transmis dans le modèle de fonction ;
  3. std::forward est une fonction de modèle utilisée pour transférer des références en tant que références rvalue ou lvalue à l'intérieur d'un modèle de fonction.

Ce qui suit montre l'utilisation de ce modèle de fonction :

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

Le résultat de l'exécution du programme est :

Grâce à un transfert parfait, nous pouvons gérer correctement la catégorie de valeur du paramètre réel transmis dans le modèle de fonction et la transmettre à la fonction interne pour obtenir la conservation complète du type et de la catégorie de valeur et améliorer la flexibilité et l'efficacité du code.


Résumer

Après avoir appris cela, certains lecteurs peuvent ne pas être en mesure de se rappeler clairement si les références lvalue et les références rvalue peuvent faire référence à des lvalues ​​​​ou à des rvalues. Voici un tableau que tout le monde doit retenir :
 

  •  Dans le tableau, Y signifie pris en charge et N signifie non pris en charge.

Ce qui précède contient toutes les connaissances sur les références lvalue et les références rvalue ! Merci à tous d'avoir regardé et soutenu ! ! !

Je suppose que tu aimes

Origine blog.csdn.net/m0_56069910/article/details/132475156
conseillé
Classement