Inicialización en C ++ moderno


Es bien sabido que la semántica de inicialización es una de las partes más complejas de C ++. Hay muchos tipos de inicialización, descritos por diferentes sintaxis, y todos interactúan de una manera compleja y desafiante. C ++ 11 trajo el concepto de "inicialización universal". Desafortunadamente, ella introdujo reglas aún más complejas y, a su vez, fueron bloqueadas en C ++ 14, C ++ 17 y cambiaron nuevamente en C ++ 20.


Bajo el corte: video y traducción del informe de Timur Doumler de la conferencia C ++ Rusia . Timur primero resume los resultados históricos de la evolución de la inicialización en C ++, ofrece una visión general sistemática de la versión actual de la regla de inicialización, problemas y sorpresas típicas, explica cómo usar todas estas reglas de manera efectiva y finalmente habla de nuevas propuestas en el estándar que pueden hacer que la inicialización sea semántica C ++ 20 es un poco más conveniente. Además, la historia está en su nombre.



Tabla de contenidos




El gif que ves ahora transmite muy bien el mensaje principal del informe. Lo encontré en Internet hace unos seis meses y lo publiqué en mi Twitter. En los comentarios a ella, alguien dijo que faltan tres tipos más de inicialización. Comenzó una discusión, durante la cual fui invitado a informar sobre esto. Y así comenzó todo.


Sobre la inicialización, Nikolay Yossutis ya dijo . Su informe incluía una diapositiva que enumeraba 19 formas diferentes de inicializar un int:


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 

Me parece que esta es una situación única para un lenguaje de programación. Inicializar una variable es una de las acciones más simples, pero en C ++ no es nada fácil de hacer. Es poco probable que este lenguaje tenga otra área en la que en los últimos años haya habido tantos informes de desviaciones del estándar, correcciones y cambios. Las reglas de inicialización cambian de estándar a estándar, y hay innumerables publicaciones en Internet sobre cómo inicializar en C ++. Por lo tanto, hacer una revisión sistemática es una tarea no trivial.


Presentaré el material en orden cronológico: primero hablaremos de lo que se heredó de C, luego de C ++ 98, luego de C ++ 03, C ++ 11, C ++ 14 y C ++ 17. Discutiremos los errores comunes y daré mis recomendaciones con respecto a la inicialización adecuada. También hablaré sobre las innovaciones en C ++ 20. Se presentará una tabla general al final del informe.



Inicialización predeterminada (C)


En C ++, muchas cosas se heredan de C, por eso comenzaremos con él. Hay varias formas de inicializar variables en C. Es posible que no se inicialicen en absoluto, y esto se denomina inicialización predeterminada . En mi opinión, este es un nombre desafortunado. El hecho es que a ninguna variable se le asigna un valor predeterminado, simplemente no se inicializa. Si recurre a una variable no inicializada en C ++ y C, obtendrá un comportamiento indefinido:


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

Lo mismo se aplica a los tipos personalizados: si en alguna struct hay campos no inicializados, al acceder a ellos, también se produce un comportamiento indefinido:


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

Se han agregado muchas construcciones nuevas a C ++: clases, constructores, métodos públicos, privados, pero nada de esto afecta el comportamiento que se acaba de describir. Si algún elemento no se inicializa en la clase, cuando se accede a él, se produce un comportamiento indefinido:


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

No hay una forma mágica de inicializar un elemento de clase en C ++ de forma predeterminada. Este es un punto interesante, y durante los primeros años de mi carrera con C ++, no lo sabía. Ni el compilador ni el IDE, que usé entonces, me recordaron esto de ninguna manera. Mis colegas no prestaron atención a esta característica al verificar el código. Estoy bastante seguro de que debido a ella, hay algunos errores bastante extraños en mi código escritos durante estos años. Me pareció obvio que las clases deberían inicializar sus variables.


En C ++ 98, puede inicializar variables utilizando la lista de inicializador de miembros. Pero tal solución al problema no es óptima, ya que debe hacerse en cada constructor, y esto es fácil de olvidar. Además, la inicialización continúa en el orden en que se declaran las variables, y no en el orden de la lista de inicializadores de miembros:


 // 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, se agregaron inicializadores de miembros directos, que son mucho más convenientes de usar. Le permiten inicializar todas las variables al mismo tiempo, y esto le da confianza de que todos los elementos se inicializan:


 // 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(); } 

Mi primera recomendación: siempre que pueda, use siempre DMI (inicializadores de miembros directos). Se pueden usar tanto con tipos integrados ( float e int ) como con objetos. El hábito de inicializar elementos nos hace abordar este tema de manera más consciente.



Inicialización de copia (C)


Entonces, el primer método de inicialización heredado de C es la inicialización por defecto, y no debe usarse. La segunda forma es la inicialización de la copia . En este caso, indicamos la variable y, a través del signo igual, su valor:


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

La inicialización de copia también se usa cuando se pasa un argumento a una función por valor, o cuando un objeto se devuelve de una función por valor:


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

Un signo igual puede dar la impresión de que se está asignando un valor, pero esto no es así. La inicialización de copia no es una asignación de valor. No habrá nada sobre apropiación en este informe.


Otra propiedad importante de la inicialización de copia: si los tipos de valores no coinciden, se ejecuta una secuencia de conversión. Una secuencia de conversión tiene ciertas reglas, por ejemplo, no llama a constructores explícitos, ya que no son constructores transformadores. Por lo tanto, si realiza una inicialización de copia para un objeto cuyo constructor está marcado como explícito, se produce un error de compilación:


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

Además, si hay otro constructor que no es explícito, pero es peor en tipo, entonces la inicialización de copia lo llamará, ignorando el constructor explícito:


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


Inicialización agregada (C)


El tercer tipo de inicialización de la que me gustaría hablar es la inicialización agregada . Se ejecuta cuando la matriz se inicializa con una serie de valores entre llaves:


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

Si no especifica el tamaño de la matriz, se deriva del número de valores entre paréntesis:


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

La misma inicialización se usa para las clases agregadas, es decir, las clases que son solo una colección de elementos públicos (hay algunas reglas más en la definición de clases agregadas, pero ahora no nos detendremos en ellas):


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

Esta sintaxis funcionó incluso en C y C ++ 98 y, comenzando con C ++ 11, puede omitir el signo igual en ella:


 Widget widget{1, 3.14159}; 

La inicialización agregada en realidad usa la inicialización de copia para cada elemento. Por lo tanto, si intenta utilizar la inicialización agregada (tanto con un signo igual como sin él) para varios objetos con constructores explícitos, se realiza una inicialización de copia para cada objeto y se produce un error de compilación:


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

Y si hay otro constructor para estos objetos, no explícito, se llama, incluso si es peor para escribir:


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

Consideremos una propiedad más de inicialización agregada. Pregunta: ¿qué valor devuelve este programa?


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

Texto oculto

Así es, cero. Si omite algunos elementos en una matriz de valores durante la inicialización agregada, las variables correspondientes se establecerán en cero. Esta es una propiedad muy útil, porque gracias a ella nunca puede haber elementos sin inicializar. Funciona con clases agregadas y con matrices:


 //     int[100] = {}; 

Otra propiedad importante de la inicialización agregada es la omisión de paréntesis (elisión de paréntesis). ¿Qué valor crees que devuelve este programa? Tiene un Widget , que es un agregado de dos valores int , y Thingy , un agregado de Widget e int . ¿Qué obtenemos si le pasamos dos valores de inicialización: {1, 2} ?


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

Texto oculto

La respuesta es cero. Aquí estamos tratando con un subaggregate, es decir, con una clase de agregado anidado. Dichas clases se pueden inicializar utilizando paréntesis anidados, pero puede omitir uno de estos pares de paréntesis. En este caso, se realiza un recorrido recursivo del sub-agregado y {1, 2} resulta ser equivalente a {{1, 2}, 0} . Es cierto que esta propiedad no es del todo obvia.



Inicialización Estática (C)


Finalmente, la inicialización estática también se hereda de C: las variables estáticas siempre se inicializan. Esto se puede hacer de varias maneras. Una variable estática se puede inicializar con una expresión constante. En este caso, la inicialización ocurre en tiempo de compilación. Si no asigna ningún valor a la variable, entonces se inicializa a cero:


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

Este programa devuelve 3 aunque j no j inicializado. Si la variable se inicializa no por una constante, sino por un objeto, pueden surgir problemas.


Aquí hay un ejemplo de una biblioteca real en la que estaba trabajando:


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

Había una clase de color en él, y los colores primarios (rojo, verde, azul) se definieron como objetos estáticos. Esta es una acción válida, pero tan pronto como aparece otro objeto estático en el inicializador del cual se usa el red , aparece la incertidumbre porque no hay un orden rígido en el que se inicializan las variables. Su aplicación puede acceder a una variable no inicializada y luego se bloquea. Afortunadamente, en C ++ 11 se hizo posible usar el constructor constexpr , y luego nos enfrentamos a una inicialización constante. En este caso, no hay problemas con el orden de inicialización.


Entonces, cuatro tipos de inicialización se heredan del lenguaje C: inicialización predeterminada, copia, inicialización estática y agregada.



Inicialización directa (C ++ 98)


Pasemos a C ++ 98. Quizás la característica más importante que distingue a C ++ de C son los constructores. Aquí hay un ejemplo de una llamada de constructor:


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

Usando la misma sintaxis, puede inicializar tipos integrados como int y float . Esta sintaxis se llama inicialización directa . Siempre se ejecuta cuando tenemos un argumento entre paréntesis.


Para los tipos integrados ( int , bool , float ) no hay diferencia con la inicialización de la copia aquí. Si hablamos de tipos de usuario, entonces, a diferencia de la inicialización de copia, con inicialización directa, puede pasar varios argumentos. En realidad, por el bien de esto, se inventó la inicialización directa.


Además, una inicialización directa no ejecuta una secuencia de conversión. En cambio, se llama al constructor utilizando la resolución de sobrecarga. La inicialización directa tiene la misma sintaxis que una llamada a función, y usa la misma lógica que otras funciones de C ++.


Por lo tanto, en la situación con un constructor explícito, la inicialización directa funciona bien, aunque la inicialización de copia arroja un error:


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

En una situación con dos constructores, uno de los cuales es explícito y el segundo es menos adecuado en tipo, el primero se llama con inicialización directa y el segundo con la copia. En esta situación, cambiar la sintaxis conducirá a una llamada a otro constructor; esto a menudo se olvida:


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

La inicialización directa siempre se usa cuando se usan paréntesis, incluso cuando la notación de invocación del constructor se usa para inicializar un objeto temporal, así como en new expresiones con un inicializador entre paréntesis y expresiones de cast :


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

Esta sintaxis existe mientras C ++ exista, y tiene una falla importante que Nikolai mencionó en su discurso de apertura : el análisis más irritante . Esto significa que todo lo que el compilador puede leer como una declaración (declaración), se lee exactamente como una declaración.


Considere un ejemplo en el que hay una clase Widget y una clase Thingy , y un constructor Thingy que recibe un Widget :


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

A primera vista, parece que tras la inicialización de Thingy , se le pasa el Widget predeterminado creado, pero de hecho, la función se declara aquí. Este código declara una función que recibe otra función como entrada, que no recibe nada como entrada y devuelve un Widget , y la primera función devuelve Thingy . El código se compila sin errores, pero es poco probable que busquemos ese comportamiento.



Inicialización de valor (C ++ 03)


Pasemos a la próxima versión: C ++ 03. En general, se acepta que no hubo cambios significativos en esta versión, pero esto no es así. En C ++ 03, apareció la inicialización de valores, en la que se escriben paréntesis vacíos:


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

En C ++ 98, el comportamiento indefinido ocurre aquí porque la inicialización se lleva a cabo de manera predeterminada, y comenzando con C ++ 03, este programa devuelve cero.


La regla es esta: si hay un constructor predeterminado definido por el usuario, la inicialización con un valor llama a este constructor; de lo contrario, se devuelve cero.


Consideremos con más detalle la situación con el constructor personalizado:


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

En este programa, la función inicializa el valor para el nuevo Widget y lo devuelve. Llamamos a esta función y accedemos al elemento i del objeto Widget . Desde C ++ 03, el valor de retorno aquí es cero, ya que no hay un constructor predeterminado definido por el usuario. Y si tal constructor existe, pero no inicializa i , entonces obtenemos un comportamiento indefinido:


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

Vale la pena señalar que "definido por el usuario" no significa "definido por el usuario". Esto significa que el usuario debe proporcionar el cuerpo del constructor, es decir, llaves. Si en el ejemplo anterior, reemplace el cuerpo del constructor con = default (esta característica se agregó en C ++ 11), el significado del programa cambia. Ahora tenemos un constructor definido por el usuario (definido por el usuario), pero no proporcionado por el usuario (proporcionado por el usuario), por lo que el programa devuelve cero:


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

Ahora intentemos Widget() = default fuera de la clase. El significado del programa ha cambiado nuevamente: Widget() = default se considera un constructor proporcionado por el usuario si está fuera de la clase. El programa devuelve un comportamiento indefinido nuevamente.


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

Hay una cierta lógica: un constructor definido fuera de una clase puede estar dentro de otra unidad de traducción. Es posible que el compilador no vea este constructor, ya que puede estar en otro archivo .cpp . Por lo tanto, el compilador no puede sacar ninguna conclusión sobre dicho constructor, y no puede distinguir un constructor con un cuerpo de un constructor con = default .



Inicialización universal (C ++ 11)


Hubo muchos cambios muy importantes en C ++ 11. En particular, se introdujo la inicialización universal (uniforme), que prefiero llamar "inicialización de unicornio" porque es simplemente mágico. Veamos por qué apareció ella.


Como ya notó, en C ++ hay muchas sintaxis de inicialización diferentes con diferentes comportamientos. El análisis desconcertante con paréntesis causó muchos inconvenientes. A los desarrolladores tampoco les gustó que la inicialización agregada pudiera usarse solo con matrices, pero no con contenedores como std::vector . En su lugar, tuvo que ejecutar .reserve y .push_back , o usar todo tipo de bibliotecas espeluznantes:


 //    ,  : 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); 

Los creadores del lenguaje intentaron resolver todos estos problemas introduciendo sintaxis con llaves pero sin un signo igual. Se supuso que esta sería una sintaxis única para todos los tipos, en la que se usan llaves y no hay ningún problema de análisis molesto. En la mayoría de los casos, esta sintaxis hace su trabajo.


Esta nueva inicialización se llama inicialización de lista , y viene en dos tipos: directa y copia. En el primer caso, solo se usan llaves, en el segundo - llaves con un signo igual:


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

La lista utilizada para la inicialización se llama braced-init-list . Es importante que esta lista no sea un objeto; no tiene ningún tipo. Cambiar a C ++ 11 desde versiones anteriores no crea ningún problema con los tipos agregados, por lo que este cambio no es crítico. Pero ahora la lista entre llaves tiene nuevas características. Aunque no tiene un tipo, puede ocultarse convertido a std::initializer_list , es un tipo nuevo tan especial. Y si hay un constructor que acepta std::initializer_list como entrada, entonces este constructor se llama:


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

Me parece que desde el lado del comité de C ++, std::initializer_list no fue la solución más exitosa. De él más daño que bien.


Para empezar, std::initializer_list es un vector de tamaño fijo con elementos const . Es decir, es un tipo, tiene funciones de begin y end que los iteradores devuelven, tiene su propio tipo de iterador, y para usarlo, debe incluir un encabezado especial. Como los elementos std::initializer_list son const , no se pueden mover, por lo que si T en el código anterior es de tipo solo movimiento, el código no se ejecutará.


A continuación, std::initializer_list es un objeto. Usándolo, de hecho, creamos y transferimos objetos. Como regla, el compilador puede optimizar esto, pero desde el punto de vista de la semántica, todavía tratamos con objetos innecesarios.


Hace unos meses hubo una encuesta en Twitter: si pudieras retroceder en el tiempo y eliminar algo de C ++, ¿qué eliminarías? La mayoría de todos los votos recibieron exactamente 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_listinitializer_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/469465/


All Articles