Nouvelles fonctionnalités C++11 ② | lvalue, référence lvalue, rvalue et référence rvalue

Table des matières

1. Introduction

2. Catégories de valeurs et concepts associés

3. Valeur gauche, valeur droite

4. référence lvalue, référence rvalue

5. Sémantique mobile

5.1. Pourquoi déplacer la sémantique est nécessaire

5.2. Définition de la sémantique mobile

5.3. Constructeur de transfert

5.4. Fonction d'affectation de transfert

6. Fonction de bibliothèque standard std::move

7. Transfert parfait std :: forward


Résumé du développement des fonctions communes de VC++ (liste des articles de chroniques, bienvenue pour s'abonner, mises à jour continues...) icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585 Série de tutoriels de dépannage d'anomalies logicielles C++ de entrée à la compétence (liste des articles de la colonne), bienvenue pour vous abonner et continuer à mettre à jour...) icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931 Outils d'analyse de logiciels C++ de l'entrée à la collection de cas de maîtrise (colonne l'article est en cours de mise à jour...) icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795 Bases et avancées du C/C++ (article de chronique, mis à jour continuellement...) icon-default.png?t=N7T8https://blog.csdn.net /chenlycly/category_11931267.html        Les nouvelles fonctionnalités de C++ 11 sont très importantes. En tant que développeur C++, il est nécessaire de les apprendre. Non seulement elles seront abordées lors d'examens écrits et d'entretiens, mais elles seront également utilisées à grande échelle en milieu ouvert. code source. Prenons comme exemple le projet open source WebRTC utilisé par de nombreux logiciels de visioconférence et de diffusion en direct. Le code WebRTC utilise largement les nouvelles fonctionnalités de C++11 et supérieur. Pour comprendre son code source, vous devez comprendre ces nouvelles fonctionnalités de C++. Par conséquent, dans la prochaine période, je vous donnerai une explication détaillée des nouvelles fonctionnalités de C++11 basée sur la pratique de travail à titre de référence.

1. Introduction

       C++11 introduit le concept de mouvement d'objet, qui est la possibilité de déplacer plutôt que de copier des objets. Le déplacement d'objets peut améliorer efficacement les performances du programme.

       Pour prendre en charge les opérations de déplacement, C++11 introduit un nouveau type de référence : les références rvalue. La référence dite rvalue est une référence qui doit être liée à une rvalue. La référence rvalue est obtenue via l'opérateur &&. Aujourd'hui, parlons en détail des lvalues, des références lvalue, des rvalues ​​et des références rvalue.

2. Catégories de valeurs et concepts associés

       En C++ 11, les catégories de valeurs sont principalement divisées en lvalue, lvalue référence, rvalue et rvalue référence. Une référence lvalue est une application liée à une lvalue, et une référence rvalue est une référence liée à une rvalue. Lorsque la référence rvalue T&& apparaît dans les paramètres de la fonction modèle, T est le type de modèle et T&& est la référence universelle. Les références universelles peuvent accepter les paramètres lvalue ainsi que les paramètres rvalue. L'effondrement des références peut alors se produire lorsque les types de paramètres de fonction sont déduits. Cela implique également les fonctions de bibliothèque standard std::move et std::forward.

3. Valeur gauche, valeur droite

       En langage C, on mentionne souvent lvalue (lvalue) et rvalue (rvalue). L'une des méthodes d'identification les plus courantes est que dans une expression d'affectation, ce qui apparaît à gauche du signe égal est une « valeur », et ce qui apparaît à droite du signe égal est appelé une « valeur ». comme:

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

Dans cette expression d’affectation, a est une lvalue et b + c est une rvalue.

       Cependant, il existe un autre dicton largement reconnu en C++, à savoir que ceux qui peuvent prendre des adresses et avoir des noms sont des lvalues, et inversement, ceux qui ne peuvent pas prendre d'adresses et n'ont pas de noms sont des rvalues. Ensuite, dans cette expression d'affectation supplémentaire, &a est une opération autorisée, mais des opérations comme &(b + c) ne réussiront pas la compilation. Donc a est une lvalue et (b + c) est une rvalue. Contrairement aux lvalues, les rvalues ​​​​représentent des constantes littérales, des expressions, des valeurs de retour non références de fonctions, etc.

       Pour résumer ici, qu'est-ce qu'une lvalue :

1) Peut se voir attribuer une valeur (conditions suffisantes mais pas nécessaires), les lvalues ​​​​ne peuvent pas nécessairement être attribuées, comme les variables const, l'intelligence attribue une valeur initiale lors de l'initialisation et ne peut pas être attribuée ultérieurement 2) Peut accéder aux adresses (suffisantes
mais pas de conditions nécessaires), les valeurs peuvent ne pas l'être. Elle peut être adressée, comme la variable de registre en langage C : registre int i= 3, C++11 a annulé la prise en charge du registre, et elle sera ignorée lors de la compilation. Pour un autre exemple, les variables de champ de bits en langage C ne peuvent pas être adressées :
struct St{ int m:3;} St st; st.m = 3; spécifie que le membre de type int m occupe 3 octets.
3) Une référence lvalue peut être initialisée (une condition nécessaire mais pas suffisante). Une lvalue peut initialiser une référence lvalue, par exemple : int m = 3 ; int& n = m;, et une rvalue ne peut pas initialiser une référence lvalue, par exemple. : const int& m = 3 ; Parce que 3 est une rvalue, la référence lvalue ne peut pas être initialisée, donc la compilation signalera une erreur.
4) Les littéraux sont de pures valeurs r. Par exemple, les nombres immédiats tels que 1, 2 et 3 sont des littéraux. Notez que la chaîne constante "xyz" est une lvalue et que l'adresse de cette chaîne peut être prise, c'est-à-dire &" xyz".
5) Les ressources en valeur mourante peuvent être volées. La valeur de retour de la fonction est une rvalue pure et renvoie une référence rvalue, telle que la fonction std::move.

4. référence lvalue, référence rvalue

       Une référence lvalue est un type qui fait référence à une lvalue, et une référence rvalue est un type qui fait référence à une rvalue. Les références lvalue et les références rvalue sont des types référence. Qu'une référence lvalue ou une référence rvalue soit déclarée, elle doit être initialisée immédiatement. La raison peut être comprise comme étant que le type référence lui-même ne possède pas la mémoire de l'objet lié, mais n'est qu'un alias de l'objet.

        Une référence lvalue est un alias pour une valeur de variable nommée, tandis qu'une référence rvalue est un alias pour une variable sans nom (anonyme).

        Exemple de référence lvalue :

int &a = 2;       // 左值引用绑定到右值,编译失败, err
int b = 2;        // 非常量左值
const int &c = b; // 常量左值引用绑定到非常量左值,编译通过, ok
const int d = 2;  // 常量左值
const int &e = c; // 常量左值引用绑定到常量左值,编译通过, ok
const int &b = 2; // 常量左值引用绑定到右值,编程通过, ok

"const type&" est un type de référence "universel", qui peut accepter des lvalues ​​non const, des lvalues ​​constantes et des rvalues ​​​​pour l'initialisation ;

       référence rvalue, représentée par && :

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

       Normalement, les références rvalue ne peuvent être liées à aucune lvalue :

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

       Regardons un exemple de test :

void process_value(int & i) //参数为左值引用
{
    cout << "LValue processed: " << i << endl;
}

void process_value(int && i) //参数为右值引用
{
    cout << "RValue processed: " << i << endl;
}

int main()
{
    int a = 0;
    process_value(a); //LValue processed: 0
    process_value(1); //RValue processed: 1

    return 0;
}

5. Sémantique mobile

5.1. Pourquoi déplacer la sémantique est nécessaire

       Les références Rvalue sont utilisées pour prendre en charge la sémantique de transfert. La sémantique de transfert peut transférer des ressources (tas, objets système, etc.) d'un objet à un autre, ce qui peut réduire la création, la copie et la destruction inutiles d'objets temporaires et améliorer considérablement les performances des applications C++. La maintenance (création et destruction) des objets temporaires a un impact important sur les performances.

       La sémantique de transfert est opposée à la sémantique de copie, et peut être comparée à la découpe et à la copie de fichiers. Lorsque l'on copie un fichier d'un répertoire à un autre, la vitesse est beaucoup plus lente que la découpe. Grâce à la sémantique de transfert, les ressources des objets temporaires peuvent être transférées vers d'autres objets.

5.2. Définition de la sémantique mobile

       Dans le mécanisme C++ existant, nous pouvons définir un constructeur de copie et une fonction d'affectation. Pour implémenter la sémantique de transfert, un constructeur de transfert doit être défini, et un opérateur d'affectation de transfert peut également être défini. Pour copier et attribuer des rvalues, le constructeur de transfert et l'opérateur d'affectation de transfert sont appelés.

       Si le constructeur de transfert et l'opérateur de copie de transfert ne sont pas définis, alors le mécanisme existant est suivi et le constructeur de copie et l'opérateur d'affectation seront appelés. Les fonctions et opérateurs ordinaires peuvent également utiliser des opérateurs de référence rvalue pour implémenter la sémantique de transfert.

5.3. Constructeur de transfert

       Regardons d'abord un exemple de constructeur de transfert :

class MyString
{
public:
    MyString(const char *tmp = "abc")
    {//普通构造函数
        len = strlen(tmp);  //长度
        str = new char[len+1]; //堆区申请空间
        strcpy(str, tmp); //拷贝内容

        cout << "普通构造函数 str = " << str << endl;
    }

    MyString(const MyString &tmp)
    {//拷贝构造函数
        len = tmp.len;
        str = new char[len + 1];
        strcpy(str, tmp.str);

        cout << "拷贝构造函数 tmp.str = " << tmp.str << endl;
    }

    //移动构造函数
    //参数是非const的右值引用
    MyString(MyString && t)
    {
        str = t.str; //拷贝地址,没有重新申请内存
        len = t.len;

        //原来指针置空
        t.str = NULL;
        cout << "移动构造函数" << endl;
    }

    MyString &operator= (const MyString &tmp)
    {//赋值运算符重载函数
        if(&tmp == this)
        {
            return *this;
        }

        //先释放原来的内存
        len = 0;
        delete []str;

        //重新申请内容
        len = tmp.len;
        str = new char[len + 1];
        strcpy(str, tmp.str);

         cout << "赋值运算符重载函数 tmp.str = " << tmp.str << endl;

        return *this;

    }

    ~MyString()
    {//析构函数
        cout << "析构函数: ";
        if(str != NULL)
        {
            cout << "已操作delete, str =  " << str;
            delete []str;
            str = NULL;
            len = 0;

        }
        cout << endl;
    }

private:
    char *str = NULL;
    int len = 0;
};

MyString func() //返回普通对象,不是引用
{
    MyString obj("mike");

    return obj;
}

int main()
{
    MyString &&tmp = func(); //右值引用接收

    return 0;
}


Semblable au constructeur de copie, il y a quelques points à noter :

1) Le symbole du paramètre (rvalue) doit être un symbole de référence rvalue, à savoir "&&".
2) Le paramètre (rvalue) ne peut pas être une constante, car nous devons modifier la rvalue.
3) Les liens de ressources et les balises des paramètres (rvalue) doivent être modifiés, sinon le destructeur de la rvalue libérera les ressources, et les ressources transférées vers le nouvel objet seront invalides.

       Avec la référence rvalue et la sémantique de transfert, lorsque nous concevons et implémentons des classes, pour les classes qui doivent postuler dynamiquement pour un grand nombre de ressources, nous devons concevoir des constructeurs de transfert et des fonctions d'affectation de transfert pour améliorer l'efficacité de l'application. 

5.4. Fonction d'affectation de transfert

         Regardons directement l'exemple de la fonction d'affectation de transfert :

class MyString
{
public:
    MyString(const char *tmp = "abc")
    {//普通构造函数
        len = strlen(tmp);  //长度
        str = new char[len+1]; //堆区申请空间
        strcpy(str, tmp); //拷贝内容

        cout << "普通构造函数 str = " << str << endl;
    }

    MyString(const MyString &tmp)
    {//拷贝构造函数
        len = tmp.len;
        str = new char[len + 1];
        strcpy(str, tmp.str);

        cout << "拷贝构造函数 tmp.str = " << tmp.str << endl;
    }

    //移动构造函数
    //参数是非const的右值引用
    MyString(MyString && t)
    {
        str = t.str; //拷贝地址,没有重新申请内存
        len = t.len;

        //原来指针置空
        t.str = NULL;
        cout << "移动构造函数" << endl;
    }

    MyString &operator= (const MyString &tmp)
    {//赋值运算符重载函数
        if(&tmp == this)
        {
            return *this;
        }

        //先释放原来的内存
        len = 0;
        delete []str;

        //重新申请内容
        len = tmp.len;
        str = new char[len + 1];
        strcpy(str, tmp.str);

         cout << "赋值运算符重载函数 tmp.str = " << tmp.str << endl;

        return *this;

    }

    //移动赋值函数
    //参数为非const的右值引用
    MyString &operator=(MyString &&tmp)
    {
        if(&tmp == this)
        {
            return *this;
        }

        //先释放原来的内存
        len = 0;
        delete []str;

        //无需重新申请堆区空间
        len = tmp.len;
        str = tmp.str; //地址赋值
        tmp.str = NULL;

        cout << "移动赋值函数\n";

        return *this;
    }

    ~MyString()
    {//析构函数
        cout << "析构函数: ";
        if(str != NULL)
        {
            cout << "已操作delete, str =  " << str;
            delete []str;
            str = NULL;
            len = 0;

        }
        cout << endl;
    }

private:
    char *str = NULL;
    int len = 0;
};

MyString func() //返回普通对象,不是引用
{
    MyString obj("mike");

    return obj;
}

int main()
{
    MyString tmp("abc"); //实例化一个对象
    tmp = func();

    return 0;
}

6. Fonction de bibliothèque standard std::move

       Le compilateur ne peut appeler le constructeur de transfert et la fonction d'affectation de transfert que pour les références rvalue, et tous les objets nommés ne peuvent être que des références lvalue. Si vous savez qu'un objet nommé n'est plus utilisé et que vous souhaitez appeler le constructeur de transfert et transférer l'affectation sur celui-ci Fonction, c'est-à-dire utiliser une référence lvalue comme référence rvalue, comment faire ? La bibliothèque standard fournit la fonction std::move, qui convertit une référence lvalue en référence rvalue d'une manière très simple.

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

7. Transfert parfait std :: forward

        Le transfert parfait convient aux scénarios dans lesquels vous devez transmettre un ensemble de paramètres à une autre fonction sans modification. "Inchangé" ne signifie pas seulement que la valeur du paramètre reste inchangée. En C++, en plus de la valeur du paramètre, il existe deux ensembles d'attributs : lvalue/rvalue et const/non-const. Un transfert parfait signifie que pendant le processus de transmission des paramètres, tous ces attributs et valeurs de paramètres ne peuvent pas être modifiés, et en même temps, aucune surcharge supplémentaire n'est encourue, comme si le transitaire n'existait pas. Dans les fonctions génériques, de telles exigences sont très courantes.

       Voici quelques exemples:

#include <iostream>
using namespace std;

template <typename T> void process_value(T & val)
{
    cout << "T &" << endl;
}

template <typename T> void process_value(const T & val)
{
    cout << "const T &" << endl;
}
//函数 forward_value 是一个泛型函数,它将一个参数传递给另一个函数 process_value
template <typename T> void forward_value(const T& val)
{
    process_value(val);
}

template <typename T> void forward_value(T& val)
{
    process_value(val);
}

int main()
{
    int a = 0;
    const int &b = 1;

    //函数 forward_value 为每一个参数必须重载两种类型,T& 和 const T&
    forward_value(a); // T&
    forward_value(b); // const T &
    forward_value(2); // const T&

    return 0;
}

        Pour un paramètre, il doit être surchargé deux fois, c'est-à-dire que le nombre de surcharges de fonctions est directement proportionnel au nombre de paramètres. Le nombre de définitions de cette fonction est très inefficace pour les programmeurs.

        Alors, comment C++11 résout-il le problème de la transmission parfaite ? En fait, C++11 réalise une transmission parfaite en introduisant une nouvelle règle de langage appelée « réduction des références » et en la combinant avec de nouvelles règles de dérivation de modèles.

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

        Règles de pliage de référence en C++11 :

Définition du type de TR

Déclarer le type de v

type réel de v

T &

TR

T &

T &

TR &

T &

T &

TR &&

T &

T &&

TR

T &&

T &&

TR &

T &

T &&

TR &&

T &&

Notez qu'une fois qu'une référence lvalue apparaît dans une définition, le repliement des références la réduit toujours en premier en une référence lvalue. En C++11, std::forward peut enregistrer les caractéristiques lvalue ou rvalue des paramètres :

#include <iostream>
using namespace std;

template <typename T> void process_value(T & val)
{
    cout << "T &" << endl;
}

template <typename T> void process_value(T && val)
{
    cout << "T &&" << endl;
}

template <typename T> void process_value(const T & val)
{
    cout << "const T &" << endl;
}

template <typename T> void process_value(const T && val)
{
    cout << "const T &&" << endl;
}

//函数 forward_value 是一个泛型函数,它将一个参数传递给另一个函数 process_value
template <typename T> void forward_value(T && val) //参数为右值引用
{
    process_value( std::forward<T>(val) );//C++11中,std::forward可以保存参数的左值或右值特性
}

int main()
{
    int a = 0;
    const int &b = 1;

    forward_value(a); // T &
    forward_value(b); // const T &
    forward_value(2); // T &&
    forward_value( std::move(b) ); // const T &&

    return 0;
}

Je suppose que tu aimes

Origine blog.csdn.net/chenlycly/article/details/132746812
conseillé
Classement