Initialisation en C ++ moderne


Il est bien connu que la sémantique d'initialisation est l'une des parties les plus complexes du C ++. Il existe de nombreux types d'initialisation, décrits par différentes syntaxes, et ils interagissent tous de manière complexe et difficile. C ++ 11 a apporté le concept de "l'initialisation universelle". Malheureusement, elle a introduit des règles encore plus complexes, et à leur tour, elles ont été bloquées en C ++ 14, C ++ 17 et modifiées à nouveau en C ++ 20.


Under the cut - vidéo et traduction du rapport de Timur Doumler de la conférence C ++ Russia . Timur résume d'abord les résultats historiques de l'évolution de l'initialisation en C ++, donne un aperçu systématique de la version actuelle de la règle d'initialisation, des problèmes typiques et des surprises, explique comment utiliser efficacement toutes ces règles, et parle enfin de nouvelles propositions dans la norme qui peuvent faire de la sémantique d'initialisation C ++ 20 est un peu plus pratique. De plus, l'histoire est en son nom.



Table des matières




Le gif que vous voyez maintenant transmet très bien le message principal du rapport. Je l'ai trouvé sur Internet il y a environ six mois et je l'ai affiché sur mon Twitter. Dans ses commentaires, quelqu'un a dit qu'il manquait trois autres types d'initialisation. Une discussion a commencé, au cours de laquelle j'ai été invitée à faire rapport à ce sujet. Et donc tout a commencé.


À propos de l'initialisation, Nikolay Yossutis l'a déjà dit . Son rapport comprenait une diapositive énumérant 19 façons différentes d'initialiser un entier:


int i1; //undefined value int i2 = 42; //note: inits with 42 int i3(42); //inits with 42 int i4 = int(); //inits with 42 int i5{42}; //inits with 42 int i6 = {42}; //inits with 42 int i7{}; //inits with 0 int i8 = {}; //inits with 0 auto i9 = 42; //inits with 42 auto i10{42}; //C++11: std::initializer_list<int>, C++14: int auto i11 = {42}; //inits std::initializer_list<int> with 42 auto i12 = int{42}; //inits int with 42 int i13(); //declares a function int i14(7, 9); //compile-time error int i15 = (7, 9); //OK, inits int with 9 (comma operator) int i16 = int(7, 9); //compile-time error int i17(7, 9); //compile-time error auto i18 = (7, 9); //OK, inits int with 9 (comma operator) auto i19 = int(7, 9); //compile-time error 

Il me semble que c'est une situation unique pour un langage de programmation. L'initialisation d'une variable est l'une des actions les plus simples, mais en C ++ ce n'est pas du tout facile à faire. Il est peu probable que cette langue ait un autre domaine dans lequel, au cours des dernières années, il y aurait eu autant de rapports d'écarts par rapport à la norme, de corrections et de changements. Les règles d'initialisation changent de standard en standard, et il existe d'innombrables publications sur Internet sur la façon d'initialiser en C ++. Par conséquent, en faire une revue systématique est une tâche non triviale.


Je présenterai le matériel par ordre chronologique: nous parlerons d'abord de ce qui a été hérité de C, puis de C ++ 98, puis de C ++ 03, C ++ 11, C ++ 14 et C ++ 17. Nous discuterons des erreurs courantes et je ferai mes recommandations concernant l'initialisation correcte. Je parlerai également des innovations en C ++ 20. Un tableau récapitulatif sera présenté à la toute fin du rapport.



Initialisation par défaut (C)


En C ++, beaucoup de choses sont héritées de C, c'est pourquoi nous allons commencer avec. Il existe plusieurs façons d'initialiser des variables en C. Ils peuvent ne pas être initialisés du tout, et cela s'appelle l' initialisation par défaut . À mon avis, c'est un nom malheureux. Le fait est qu'aucune variable ne reçoit de valeur par défaut, elle n'est tout simplement pas initialisée. Si vous vous tournez vers une variable non initialisée en C ++ et C, vous obtenez un comportement non défini:


 int main() { int i; return i; // undefined behaviour } 

La même chose s'applique aux types personnalisés: si dans certaines struct il y a des champs non initialisés, alors lors de leur accès, un comportement non défini se produit également:


 struct Widget { int i; int j; }; int main() { Widget widget; return widget.i; //   } 

De nombreuses nouvelles constructions ont été ajoutées à C ++: classes, constructeurs, méthodes publiques, privées, mais rien de tout cela n'affecte le comportement qui vient d'être décrit. Si un élément n'est pas initialisé dans la classe, alors lors de l'accès, un comportement indéfini se produit:


 class Widget { public: Widget() {} int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i; int j; }; int main() { Widget widget; return widget.get_i(); // Undefined behaviour! } 

Il n'y a aucun moyen magique d'initialiser un élément de classe en C ++ par défaut. C'est un point intéressant, et pendant les premières années de ma carrière avec C ++, je ne le savais pas. Ni le compilateur ni l'IDE, que j'ai utilisé à l'époque, ne me l'ont rappelé en aucune façon. Mes collègues n'ont pas fait attention à cette fonctionnalité lors de la vérification du code. Je suis presque sûr qu'à cause d'elle, il y a des bugs assez étranges dans mon code écrits pendant ces années. Il me semblait évident que les classes devraient initialiser leurs variables.


En C ++ 98, vous pouvez initialiser des variables à l'aide de la liste d'initialisation des membres. Mais une telle solution au problème n'est pas optimale, car elle doit être effectuée dans chaque constructeur, et c'est facile à oublier. De plus, l'initialisation se déroule dans l'ordre dans lequel les variables sont déclarées et non dans l'ordre de la liste d'initialisation des membres:


 // C++98: member initialiser list class Widget { public: Widget() : i(0), j(0) {} // member initialiser list int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i; int j; }; int main() { Widget widget; return widget.get_i(); } 

En C ++ 11, des initialiseurs de membres directs ont été ajoutés, ce qui est beaucoup plus pratique à utiliser. Ils vous permettent d'initialiser toutes les variables en même temps, ce qui donne l'assurance que tous les éléments sont initialisés:


 // C++11: default member initialisers class Widget { public: Widget() {} int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i = 0; // default member initialisers int j = 0; }; int main() { Widget widget; return widget.get_i(); } 

Ma première recommandation: chaque fois que vous le pouvez, utilisez toujours les DMI (initialiseurs de membres directs). Ils peuvent être utilisés à la fois avec des types intégrés ( float et int ) et avec des objets. L'habitude d'initialiser des éléments nous fait aborder cette question plus consciemment.



Initialisation de la copie (C)


Ainsi, la première méthode d'initialisation héritée de C est l'initialisation par défaut, et elle ne doit pas être utilisée. La deuxième méthode consiste à initialiser la copie . Dans ce cas, nous indiquons la variable et par le signe égal - sa valeur:


 // copy initialization int main() { int i = 2; } 

L'initialisation de copie est également utilisée lorsqu'un argument est passé à une fonction par valeur, ou lorsqu'un objet est renvoyé d'une fonction par valeur:


 // copy initialization int square(int i) { return i * i; } 

Un signe égal peut donner l'impression qu'une valeur est affectée, mais ce n'est pas le cas. L'initialisation de la copie n'est pas une affectation de valeur. Il n'y aura rien sur l'appropriation dans ce rapport.


Autre propriété importante de l'initialisation de la copie: si les types de valeurs ne correspondent pas, une séquence de conversion est exécutée. Une séquence de conversion a certaines règles, par exemple, elle n'appelle pas de constructeurs explicites, car ils ne transforment pas de constructeurs. Par conséquent, si vous effectuez une initialisation de copie pour un objet dont le constructeur est marqué comme explicite, une erreur de compilation se produit:


 struct Widget { explicit Widget(int) {} }; Widget w1 = 1; // ERROR 

De plus, s'il existe un autre constructeur qui n'est pas explicite, mais dont le type est pire, alors l'initialisation de la copie l'appellera, en ignorant le constructeur explicite:


 struct Widget { explicit Widget(int) {} Widget(double) {} }; Widget w1 = 1; //  Widget(double) 


Initialisation d'agrégat (C)


Le troisième type d'initialisation dont je voudrais parler est l'initialisation agrégée . Il est exécuté lorsque le tableau est initialisé avec une série de valeurs entre accolades:


 int i[4] = {0, 1, 2, 3}; 

Si vous ne spécifiez pas la taille du tableau, il est dérivé du nombre de valeurs entre parenthèses:


 int j[] = {0, 1, 2, 3}; // array size deduction 

La même initialisation est utilisée pour les classes agrégées, c'est-à-dire les classes qui ne sont qu'une collection d'éléments publics (il y a quelques règles de plus dans la définition des classes agrégées, mais maintenant nous ne nous attarderons pas sur elles):


 struct Widget { int i; float j; }; Widget widget = {1, 3.14159}; 

Cette syntaxe a fonctionné même en C et C ++ 98, et, à partir de C ++ 11, vous pouvez ignorer le signe égal:


 Widget widget{1, 3.14159}; 

L'initialisation agrégée utilise en fait l'initialisation de la copie pour chaque élément. Par conséquent, si vous essayez d'utiliser l'initialisation agrégée (à la fois avec un signe égal et sans lui) pour plusieurs objets avec des constructeurs explicites, une initialisation de copie est effectuée pour chaque objet et une erreur de compilation se produit:


 struct Widget { explicit Widget(int) {} }; struct Thingy { Widget w1, w2; }; int main() { Thingy thingy = {3, 4}; // ERROR Thingy thingy {3, 4}; // ERROR } 

Et s'il existe un autre constructeur pour ces objets, non explicite, alors il est appelé, même s'il est plus mal adapté à taper:


 struct Widget { explicit Widget(int) {} Widget(double) {} }; struct Thingy { Widget w1, w2; }; int main() { Thingy thingy = {3, 4}; //  Widget(double) Thingy thingy {3, 4}; //  Widget(double) } 

Considérons une autre propriété d'initialisation agrégée. Question: quelle valeur ce programme renvoie-t-il?


 struct Widget { int i; int j; }; int main() { Widget widget = {1}; return widget.j; } 

Texte masqué

C'est vrai, zéro. Si vous ignorez certains éléments dans un tableau de valeurs lors de l'initialisation agrégée, les variables correspondantes sont définies sur zéro. C'est une propriété très utile, car grâce à elle, il ne peut jamais y avoir d'éléments non initialisés. Il fonctionne avec des classes agrégées et avec des tableaux:


 //     int[100] = {}; 

Une autre propriété importante de l'initialisation d'agrégat est l'omission des crochets (accolade élision). Quelle valeur pensez-vous que ce programme rapporte? Il a un Widget , qui est un agrégat de deux valeurs int , et Thingy , un agrégat de Widget et int . Qu'obtenons-nous si nous lui transmettons deux valeurs d'initialisation: {1, 2} ?


 struct Widget { int i; int j; }; struct Thingy { Widget w; int k; }; int main() { Thingy t = {1, 2}; return tk; //   ? } 

Texte masqué

La réponse est zéro. Ici, nous avons affaire à un sous-agrégat, c'est-à-dire à une classe d'agrégats imbriquée. Ces classes peuvent être initialisées à l'aide de crochets imbriqués, mais vous pouvez ignorer l'une de ces paires de crochets. Dans ce cas, une traversée récursive du sous-agrégat est effectuée et {1, 2} s'avère être équivalent à {{1, 2}, 0} . Certes, cette propriété n'est pas tout à fait évidente.



Initialisation statique (C)


Enfin, l'initialisation statique est également héritée de C: les variables statiques sont toujours initialisées. Cela peut se faire de plusieurs manières. Une variable statique peut être initialisée avec une expression constante. Dans ce cas, l'initialisation se produit au moment de la compilation. Si vous n'affectez aucune valeur à la variable, elle est initialisée à zéro:


 static int i = 3; //   statit int j; //   int main() { return i + j; } 

Ce programme renvoie 3 même si j pas initialisé. Si la variable est initialisée non pas par une constante, mais par un objet, des problèmes peuvent survenir.


Voici un exemple d'une vraie bibliothèque sur laquelle je travaillais:


 static Colour red = {255, 0, 0}; 

Il y avait une classe Color et les couleurs primaires (rouge, vert, bleu) étaient définies comme des objets statiques. C'est une action valide, mais dès qu'un autre objet statique apparaît dans l'initialiseur dont le red est utilisé, l'incertitude apparaît car il n'y a pas d'ordre rigide dans lequel les variables sont initialisées. Votre application peut accéder à une variable non initialisée, puis elle se bloque. Heureusement, en C ++ 11, il est devenu possible d'utiliser le constructeur constexpr , puis nous avons affaire à une initialisation constante. Dans ce cas, il n'y a aucun problème avec l'ordre d'initialisation.


Ainsi, quatre types d'initialisation sont hérités du langage C: initialisation par défaut, copie, initialisation agrégée et statique.



Initialisation directe (C ++ 98)


Passons au C ++ 98. Peut-être la caractéristique la plus importante qui distingue C ++ de C est les constructeurs. Voici un exemple d'appel de constructeur:


 Widget widget(1, 2); int(3); 

En utilisant la même syntaxe, vous pouvez initialiser des types intégrés comme int et float . Cette syntaxe est appelée initialisation directe . Il est toujours exécuté lorsque nous avons un argument entre parenthèses.


Pour les types intégrés ( int , bool , float ), il n'y a pas de différence ici avec l'initialisation de la copie. Si nous parlons de types d'utilisateurs, alors, contrairement à l'initialisation de copie, avec l'initialisation directe, vous pouvez passer plusieurs arguments. En fait, pour cela, l'initialisation directe a été inventée.


De plus, une initialisation directe n'exécute pas de séquence de conversion. Au lieu de cela, le constructeur est appelé à l'aide de la résolution de surcharge. L'initialisation directe a la même syntaxe qu'un appel de fonction et utilise la même logique que les autres fonctions C ++.


Par conséquent, dans la situation avec un constructeur explicite, l'initialisation directe fonctionne correctement, bien que l'initialisation de la copie génère une erreur:


 struct Widget { explicit Widget(int) {} }; Widget w1 = 1; //  Widget w2(1); //    

Dans une situation avec deux constructeurs, dont l'un est explicite et le second est de type moins approprié, le premier est appelé avec initialisation directe et le second est appelé avec la copie. Dans cette situation, la modification de la syntaxe entraînera un appel à un autre constructeur - cela est souvent oublié:


 struct Widget { explicit Widget(int) {} Widget(double) {} }; Widget w1 = 1; //  Widget(double) Widget w2(1); //  Widget(int) 

L'initialisation directe est toujours utilisée lorsque des parenthèses sont utilisées, y compris lorsque la notation d'invocation de constructeur est utilisée pour initialiser un objet temporaire, ainsi que dans les new expressions avec un initialiseur entre crochets et dans cast expressions cast :


 useWidget(Widget(1, 2)); //   auto* widget_ptr = new Widget(2, 3); // new-expression with (args) static_cast<Widget>(thingy); // cast 

Cette syntaxe existe aussi longtemps que C ++ existe, et elle a un défaut important que Nikolai a mentionné dans son discours d'ouverture: l'analyse la plus contrariante . Cela signifie que tout ce que le compilateur peut lire comme une déclaration (déclaration), il se lit exactement comme une déclaration.


Prenons un exemple dans lequel il existe une classe Widget et une classe Thingy , et un constructeur Thingy qui reçoit un Widget :


 struct Widget {}; struct Thingy { Thingy(Widget) {} }; int main () { Thingy thingy(Widget()); } 

À première vue, il semble qu'à l'initialisation de Thingy , le Widget par défaut créé lui soit transmis, mais en fait, la fonction est déclarée ici. Ce code déclare une fonction qui reçoit une autre fonction en entrée, qui ne reçoit rien en entrée et renvoie un Widget , et la première fonction renvoie Thingy . Le code se compile sans erreur, mais il est peu probable que nous recherchions un tel comportement.



Initialisation de la valeur (C ++ 03)


Passons à la prochaine version - C ++ 03. Il est généralement admis qu'il n'y a eu aucun changement significatif dans cette version, mais ce n'est pas le cas. En C ++ 03, une initialisation de valeur est apparue, dans laquelle des parenthèses vides sont écrites:


 int main() { return int(); // UB  C++98, 0   C++03 } 

En C ++ 98, un comportement indéfini se produit ici car l'initialisation a lieu par défaut et à partir de C ++ 03, ce programme renvoie zéro.


La règle est la suivante: s'il existe un constructeur par défaut défini par l'utilisateur, l'initialisation avec une valeur appelle ce constructeur, sinon zéro est renvoyé.


Examinons plus en détail la situation avec le constructeur personnalisé:


 struct Widget { int i; }; Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; } 

Dans ce programme, la fonction initialise la valeur du nouveau Widget et la renvoie. Nous appelons cette fonction et accédons à l'élément i de l'objet Widget . Depuis C ++ 03, la valeur de retour ici est zéro, car il n'y a pas de constructeur par défaut défini par l'utilisateur. Et si un tel constructeur existe, mais n'initialise pas i , alors nous obtenons un comportement indéfini:


 struct Widget { Widget() {} //   int i; }; Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; //   ,  UB } 

Il convient de noter que «défini par l'utilisateur» ne signifie pas «défini par l'utilisateur». Cela signifie que l'utilisateur doit fournir le corps du constructeur, c'est-à-dire des accolades. Si dans l'exemple ci-dessus, remplacez le corps du constructeur par = default (cette fonctionnalité a été ajoutée en C ++ 11), la signification du programme change. Maintenant, nous avons un constructeur défini par l'utilisateur (défini par l'utilisateur), mais non fourni par l'utilisateur (fourni par l'utilisateur), donc le programme renvoie zéro:


 struct Widget { Widget() = default; // user-defined,   user-provided int i; }; Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; //  0 } 

Essayons maintenant de Widget() = default hors de la classe. La signification du programme a encore changé: Widget() = default est considéré comme un constructeur fourni par l'utilisateur s'il est en dehors de la classe. Le programme renvoie à nouveau un comportement indéfini.


 struct Widget { Widget(); int i; }; Widget::Widget() = default; //  ,  user-provided Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; //    , UB } 

Il y a une certaine logique: un constructeur défini en dehors d'une classe peut être à l'intérieur d'une autre unité de traduction. Le compilateur peut ne pas voir ce constructeur, car il peut se trouver dans un autre fichier .cpp . Par conséquent, le compilateur ne peut tirer aucune conclusion sur un tel constructeur, et il ne peut pas distinguer un constructeur avec un corps d'un constructeur avec = default .



Initialisation universelle (C ++ 11)


Il y a eu de nombreux changements très importants en C ++ 11. En particulier, une uniformisation universelle a été introduite, que je préfère appeler «initialisation de la licorne» car elle est juste magique. Voyons pourquoi elle est apparue.


Comme vous l'avez déjà remarqué, en C ++, il existe de nombreuses syntaxes d'initialisation différentes avec des comportements différents. L'analysante contrariante avec des parenthèses a causé beaucoup d'inconvénients. Les développeurs n'aimaient pas non plus que l'initialisation d'agrégat puisse être utilisée uniquement avec des tableaux, mais pas avec des conteneurs comme std::vector . Au lieu de cela, vous avez dû exécuter .reserve et .push_back , ou utiliser toutes sortes de bibliothèques effrayantes:


 //    ,  : std::vector<int> vec = {0, 1, 2, 3, 4}; //   : std::vector<int> vec; vec.reserve(5); vec.push_back(0); vec.push_back(1); vec.push_back(2); vec.push_back(3); vec.push_back(4); 

Les créateurs de la langue ont essayé de résoudre tous ces problèmes en introduisant une syntaxe avec des accolades mais sans signe égal. Il a été supposé que ce serait une seule syntaxe pour tous les types, dans laquelle des accolades sont utilisées et il n'y a pas de problème d'analyse vexing. Dans la plupart des cas, cette syntaxe fait son travail.


Cette nouvelle initialisation est appelée initialisation de liste , et elle se décline en deux types: direct et copie. Dans le premier cas, seuls les accolades sont utilisées, dans le second - accolades avec un signe égal:


 // direct-list-initialization Widget widget{1, 2}; // copy-list-initialization Widget widget = {1, 2}; 

La liste utilisée pour l'initialisation est appelée braced-init-list . Il est important que cette liste ne soit pas un objet, elle n'a pas de type. Le passage à C ++ 11 à partir de versions antérieures ne crée aucun problème avec les types d'agrégats, donc cette modification n'est pas critique. Mais maintenant, la liste entre accolades a de nouvelles fonctionnalités. Bien qu'il n'ait pas de type, il peut être caché converti en std::initializer_list , c'est un nouveau type spécial. Et s'il y a un constructeur qui accepte std::initializer_list en entrée, alors ce constructeur est appelé:


 template <typename T> class vector { //... vector(std::initializer_list<T> init); //   initializer_list }; std::vector<int> vec{0, 1, 2, 3, 4}; //  ^  

Il me semble que du côté du comité C ++, std::initializer_list n'était pas la solution la plus réussie. De lui plus de mal que de bien.


Pour commencer, std::initializer_list est un vecteur de taille fixe avec des éléments const . Autrement dit, c'est un type, il a des fonctions de begin et de end que les itérateurs retournent, il a son propre type d'itérateur, et pour l'utiliser, vous devez inclure un en-tête spécial. Puisque les éléments std::initializer_list sont const , il ne peut pas être déplacé, donc si T dans le code ci-dessus est de type move-only, le code ne sera pas exécuté.


Ensuite, std::initializer_list est un objet. En l'utilisant, nous créons et transférons des objets. En règle générale, le compilateur peut optimiser cela, mais du point de vue de la sémantique, nous traitons toujours des objets inutiles.


Il y a quelques mois, il y avait un sondage sur Twitter: si vous pouviez remonter le temps et supprimer quelque chose de C ++, que supprimeriez-vous? La plupart de tous les votes ont reçu exactement initializer_list .


https://twitter.com/shafikyaghmour/status/1058031143935561728


, initializer_list . , .


, . , initializer_list , . :


 std::vector<int> v(3, 0); //   0, 0, 0 std::vector<int> v{3, 0}; //   3, 0 

vector int , , , — . . , initializer_list , 3 0.


:


 std::string s(48, 'a'); // "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" std::string s{48, 'a'}; // "0a" 

48 «», «0». , string initializer_list . 48 , . ASCII 48 — «0». , , , int char . . , , .


. , ? ?


 template <typename T, size_t N> auto test() { return std::vector<T>{N}; } int main () { return test<std::string, 3>().size(); } 

, — 3. string int , 1, std::vector<std::int> initializer_list . initializer_list , . string int float , , . , . , emplace , . , {} .


, .



.
— ( {a} )
( = {a} );
:


  1. «» , std::initializer_list .
    — .
  2. ,
    () .

.


1: = {a} , a ,
.


2: , {} .
, initializer_list .
Widget<int> widget{}\ ?


 template Typename<T> struct Widget { Widget(); Widget(std::initializer_list<T>); }; int main() { Widget<int> widget{}; //    ? } 

, , initializer_list , initializer_list . . , , initializer_list . , . , .


{} . , -, , Widget() = default Widget() {} — .


Widget() = default :


 struct Widget { Widget() = default; int i; }; int main() { Widget widget{}; //   (),   vexing parse return widget.i; //  0 } 

Widget() {} :


 struct Widget { Widget() {}; // user-provided  int i; }; int main() { Widget widget{}; //  ,    return widget.i; //  ,  UB } 

: , (narrowing conversions). int double , , :


 int main() { int i{2.0}; // ! } 

, double . C++11, , . :


 struct Widget { int i; int j; }; int main() { Widget widget = {1.0, 0.0}; //   ++11    C++98/03 } 

, , , , (brace elision). , , . , map . map , — :


 std::map<std::string, std::int> my_map {{"abc", 0}, {"def", 1}}; 

, . :


 std::vector<std::string> v1 {"abc", "def"}; // OK std::vector<std::string> v2 {{"abc", "def"}}; // ?? 

, , initializer_list . initializer_list , , , . , . , .


initializer_list — initializer_list , . , const char* . , string , char . . , , .


:


  • ;
  • .

. braced-init-list . :


 Widget<int> f1() { return {3, 0}; // copy-list    } void f2(Widget); f2({3, 0}); // copy-list   

, , braced-init-list . braced-init-list , .


, . StackOverflow , . , . , , :


 #include <iostream> struct A { A() {} A(const A&) {} }; struct B { B(const A&) {} }; void f(const A&) { std::cout << "A" << std::endl; } void f(const B&) { std::cout << "B" << std::endl; } int main() { A a; f( {a} ); // A f( {{a}} ); // ambiguous f( {{{a}}} ); // B f({{{{a}}}}); // no matching function } 


++14


, C++11 . , , . C++14. , .


, ++11 direct member initializers, . , direct member initializers . ++14, direct member initializers:


 struct Widget { int i = 0; int j = 0; }; Widget widget{1, 2}; //    C++14 

, auto . ++11 auto braced-init-list, std::initializer_list :


 int i = 3; // int int i(3); // int int i{3}; // int int i = {3}; // int auto i = 3; // int auto i(3); // int auto i{3}; //  ++11 — std::initializer_list<int> auto i = {3}; //  ++11 — std::initializer_list<int> 

: auto i{3} , int , std::initializer_list<int> . ++14 , auto i{3} int . , . , auto i = {3} std::initializer_list<int> . , : int , — initializer_list .


 auto i = 3; // int auto i(3); // int auto i{3}; //  ++14 — int,         auto i = {3}; //    std::initializer_list<int> 

, C++14 , , , , . , .


, ++14 :


  • , , std::initializer_list .


  • std::initializer_list move-only .


  • c , emplace make_unique .


  • , :


    • , -;
    • ;
    • auto .

  • , , .



: assert(Widget(2,3)) , assert(Widget{2,3}) . , , , . , . .



C++


, ++.


int , . . — , .


: , , std::initializer_list , direct member initializers. , .


, Ă© . .


 struct Point { int x = 0; int y = 0; }; setPosition(Point{2, 3}); takeWidget(Widget{}); 

braced-init-list — .


 setPosition({2, 3}); takeWidget({}); 

, , . , — , . , , , , , . , , initializer_list . : , , .


:


  • = value


  • = {args} = {} :


    • std::initializer_list
    • direct member initialisation ( (args) )

  • {args} {} Ă©


  • (args)



, (args) vexing parse. . 2013 , , auto . , : auto i; — . , :


 auto widget = Widget(2, 3); 

, . , , vexing parse:


 auto thingy = Thingy(); 

« auto» («almost always auto», AAA), ++11 ++14 , , , std::atomic<int> :


 auto count = std::atomic<int>(0); // C++11/14:  // std::atomic is neither copyable nor movable 

, atomic . , , , , . ++17 , , (guaranteed copy elision):


 auto count = std::atomic<int>(0); // C++17: OK, guaranteed copy elision 

auto . — direct member initializers. auto .


++17 CTAD (class template argument deduction). , . . , CppCon, CTAD , . , ++17 , ++11 ++14, , . , , , , .



(++20)


++20, . , , : (designated initialization):


 struct Widget { int a; int b; int c; }; int main() { Widget widget{.a = 3, .c = 7}; }; 

, . , , . , . , b .


, , , . , .


, , 99, :


  • , , . ++ , , . :


     Widget widget{.c = 7, .a = 3}; //  

    , .


  • ++ , {.ce = 7}; , {.c{.e = 7}} :


     Widget widget{.ce = 7}; //  

  • ++ , , :


     Widget widget{.a = 3, 7}; //  

  • ++ . , -, , .


     int arr[3]{.[1] = 7}; //  



C++20


++20 , . ( wg21.link/p1008 ).


++17 , , . , , , :


 struct Widget { Widget() = delete; int i; int j; }; Widget widget1; //  Widget widget2{}; //   C++17,     C++20 

, , . ++20 . , . , . , , , .


( wg21.link/p1009 ). Braced-init-list new , : , ? — , : braced-init-list new :


 double a[]{1, 2, 3}; // OK double* p = new double[]{1, 2, 3}; //   C++17,   C++20 

, ++11 braced-init-list. ++ . , .



(C++20)


, ++20 . , . ++20 : ( wg21.link/p0960 ).


 struct Widget { int i; int j; }; Widget widget(1, 2); //   C++20 

. , emplace make_unique . . : auto , : 58.11 .


 struct Widget { int i; int j; }; auto widget = Widget(1, 2); 

, :


 int arr[3](0, 1, 2); 

, : uniform 2.0. . , , , , . — initializer_list : , , — . , . , - , — . .


, . direct member initializers. auto . direct member initializers — , . , . — , .


, , . — , — . , .



, , C++ Russia 2019 Piter «Type punning in modern C++» . , ++20, , , «» ++ , .

Source: https://habr.com/ru/post/fr469465/


All Articles