¿Por qué es mejor la inicialización de listas (usando llaves) que las alternativas?


406
MyClass a1 {a};     // clearer and less error-prone than the other three
MyClass a2 = {a};
MyClass a3 = a;
MyClass a4(a);

¿Por qué?

No pude encontrar una respuesta en SO, así que déjame responder mi propia pregunta.


12
¿Por qué no usar auto?
Mark Garcia

33
Es cierto, es conveniente, pero en mi opinión reduce la legibilidad: me gusta ver de qué tipo es un objeto al leer el código. Si está 100% seguro de qué tipo de objeto es, ¿por qué usar auto? Y si usa la inicialización de la lista (lea mi respuesta), puede estar seguro de que siempre es correcta.
Oleksiy

103
@Oleksiy: me std::map<std::string, std::vector<std::string>>::const_iteratorgustaría hablar contigo.
Xeo

99
@Oleksiy Recomiendo leer este GotW .
Rapptz

17
@doc Yo diría que using MyContainer = std::map<std::string, std::vector<std::string>>;es aún mejor (¡especialmente porque puedes
JAB

Respuestas:


357

Básicamente copiando y pegando de "The C ++ Programming Language 4th Edition" de Bjarne Stroustrup :

La inicialización de la lista no permite el estrechamiento (§iso.8.5.4). Es decir:

  • Un entero no se puede convertir a otro entero que no pueda mantener su valor. Por ejemplo, char to int está permitido, pero no int to char.
  • Un valor de punto flotante no se puede convertir a otro tipo de punto flotante que no pueda contener su valor. Por ejemplo, se permite flotar a doble, pero no doble a flotar.
  • Un valor de punto flotante no se puede convertir a un tipo entero.
  • Un valor entero no se puede convertir a un tipo de punto flotante.

Ejemplo:

void fun(double val, int val2) {

    int x2 = val; // if val==7.9, x2 becomes 7 (bad)

    char c2 = val2; // if val2==1025, c2 becomes 1 (bad)

    int x3 {val}; // error: possible truncation (good)

    char c3 {val2}; // error: possible narrowing (good)

    char c4 {24}; // OK: 24 can be represented exactly as a char (good)

    char c5 {264}; // error (assuming 8-bit chars): 264 cannot be 
                   // represented as a char (good)

    int x4 {2.0}; // error: no double to int value conversion (good)

}

La única situación en la que se prefiere = sobre {} es cuando se usa la autopalabra clave para obtener el tipo determinado por el inicializador.

Ejemplo:

auto z1 {99};   // z1 is an int
auto z2 = {99}; // z2 is std::initializer_list<int>
auto z3 = 99;   // z3 is an int

Conclusión

Prefiere la inicialización {} sobre las alternativas a menos que tengas una buena razón para no hacerlo.


52
También existe el hecho de que el uso ()se puede analizar como una declaración de función. Es confuso e inconsistente lo que puedes decir T t(x,y,z);pero no T t(). Y a veces, estás seguro x, ni siquiera puedes decirlo T t(x);.
juanchopanza

84
Estoy totalmente en desacuerdo con esta respuesta; La inicialización entre llaves se convierte en un completo desastre cuando tiene tipos con un ctor que acepta a std::initializer_list. RedXIII menciona este problema (y simplemente lo ignora), mientras que lo ignora por completo. A(5,4)y A{5,4}puede llamar a funciones completamente diferentes, y esto es algo importante para saber. Incluso puede dar lugar a llamadas que parecen poco intuitivas. Decir que deberías preferir {}por defecto hará que la gente no entienda lo que está sucediendo. Sin embargo, esto no es tu culpa. Personalmente, creo que es una característica extremadamente mal pensada.
user1520427

13
@ user1520427 Es por eso que está la parte "a menos que tenga una buena razón para no hacerlo ".
Oleksiy

67
Aunque esta pregunta es antigua, tiene bastante éxito, por lo que estoy agregando esto aquí solo como referencia (no lo he visto en ningún otro lugar de la página). Desde C ++ 14 con las nuevas Reglas para la deducción automática de braced-init-list ahora es posible escribir auto var{ 5 }y se deducirá como intno más std::initializer_list<int>.
Edoardo Sparkon Dominici

13
Jaja, de todos los comentarios todavía no está claro qué hacer. Lo que está claro es que la especificación de C ++ es un desastre.
DrumM

113

Ya hay excelentes respuestas sobre las ventajas de usar la inicialización de la lista, sin embargo, mi regla general personal es NO usar llaves siempre que sea posible, sino que dependerá del significado conceptual:

  • Si el objeto que estoy creando conceptualmente contiene los valores que paso en el constructor (por ejemplo, contenedores, estructuras de POD, atómicos, punteros inteligentes, etc.), entonces estoy usando llaves.
  • Si el constructor se parece a una llamada de función normal (realiza algunas operaciones más o menos complejas que están parametrizadas por los argumentos), entonces estoy usando la sintaxis de llamada de función normal.
  • Para la inicialización predeterminada, siempre uso llaves.
    Por un lado, de esa manera siempre estoy seguro de que el objeto se inicializa independientemente de si es, por ejemplo, una clase "real" con un constructor predeterminado que se llamaría de todos modos o un tipo incorporado / POD. En segundo lugar, es, en la mayoría de los casos, coherente con la primera regla, ya que un objeto inicializado predeterminado a menudo representa un objeto "vacío".

En mi experiencia, este conjunto de reglas se puede aplicar de manera mucho más consistente que el uso de llaves por defecto, pero tener que recordar explícitamente todas las excepciones cuando no se pueden usar o tienen un significado diferente que la sintaxis de llamada de función "normal" con paréntesis (Llama a una sobrecarga diferente).

Por ejemplo, encaja perfectamente con tipos de biblioteca estándar como std::vector:

vector<int> a{10,20};   //Curly braces -> fills the vector with the arguments

vector<int> b(10,20);   //Parentheses -> uses arguments to parametrize some functionality,                          
vector<int> c(it1,it2); //like filling the vector with 10 integers or copying a range.

vector<int> d{};      //empty braces -> default constructs vector, which is equivalent
                      //to a vector that is filled with zero elements

11
Totalmente de acuerdo con la mayoría de sus respuestas. Sin embargo, ¿no crees que poner llaves vacías para el vector es simplemente redundante? Quiero decir, está bien, cuando necesitas inicializar un objeto de tipo genérico T, pero ¿cuál es el propósito de hacerlo para un código no genérico?
Mikhail

8
@Mikhail: Ciertamente es redundante, pero es un hábito mío siempre hacer explícita la inicialización de la variable local. Como escribí, esto se trata principalmente de consistencia, así que no lo olvido, cuando importa. Sin embargo, no es nada que mencionaría en una revisión de código o en una guía de estilo.
MikeMB

44
conjunto de reglas bastante limpio.
laike9m

55
Esta es, de lejos, la mejor respuesta. {} es como herencia: fácil de abusar, lo que genera un código difícil de entender.
UKMonkey

2
Ejemplo de @MikeMB: const int &b{}<- no intenta crear una referencia no inicializada, sino que la vincula a un objeto entero temporal. Segundo ejemplo: struct A { const int &b; A():b{} {} };<- no intenta crear una referencia no inicializada (como ()lo haría), sino que la vincula a un objeto entero temporal y luego lo deja colgando. GCC incluso con -Wallno advierte para el segundo ejemplo.
Johannes Schaub - litb 18/1118

92

Hay MUCHAS razones para usar la inicialización de llaves, pero debe tener en cuenta que el initializer_list<>constructor es preferido a los otros constructores , la excepción es el constructor predeterminado. Esto conduce a problemas con los constructores y las plantillas, donde el Tconstructor de tipos puede ser una lista de inicializadores o un viejo ctor simple.

struct Foo {
    Foo() {}

    Foo(std::initializer_list<Foo>) {
        std::cout << "initializer list" << std::endl;
    }

    Foo(const Foo&) {
        std::cout << "copy ctor" << std::endl;
    }
};

int main() {
    Foo a;
    Foo b(a); // copy ctor
    Foo c{a}; // copy ctor (init. list element) + initializer list!!!
}

Suponiendo que no encuentre tales clases, hay pocas razones para no usar la lista de inicializadores.


20
Este es un punto muy importante en la programación genérica. Cuando escriba plantillas, no use las listas de inicialización (el nombre del estándar para { ... }) a menos que desee la initializer_listsemántica (bueno, y tal vez para la construcción predeterminada de un objeto).
Xeo

82
Sinceramente, no entiendo por qué std::initializer_listexiste la regla, solo agrega confusión y desorden al lenguaje. ¿Qué tiene de malo hacer Foo{{a}}si quieres el std::initializer_listconstructor? Parece mucho más fácil de entender que tener std::initializer_listprioridad sobre todas las demás sobrecargas.
user1520427

55
¡+1 por el comentario anterior, porque creo que es un desastre! no es logico; Foo{{a}}sigue algo de lógica para mí mucho más de lo Foo{a}que se convierte en la prioridad de la lista inicializadora (ups podría el usuario pensar hm ...)
Gabriel

32
Básicamente, C ++ 11 reemplaza un desastre con otro desastre. Oh, lo siento, no lo reemplaza, se agrega. ¿Cómo puedes saber si no encuentras tales clases? ¿Qué pasa si comienzas sin std::initializer_list<Foo> constructor, pero se va a agregar alFoo clase en algún momento para extender su interfaz? Entonces los usuarios de la Fooclase están jodidos.
doc

10
.. ¿Cuáles son las "MUCHAS razones para usar la inicialización de llaves"? Esta respuesta señala una razón ( initializer_list<>), que realmente no califica quién dice que es preferida, y luego procede a mencionar un buen caso donde NO es preferida. ¿Qué me estoy perdiendo que ~ otras 30 personas (a partir del 21/04/2016) encontraron útil?
dwanderson

0

Solo es más seguro siempre que no construyas con -Ninguna reducción como lo hace Google en Chromium. Si lo hace, entonces es MENOS seguro. Sin esa bandera, los únicos casos inseguros serán corregidos por C ++ 20.

Nota: A) Las llaves son más seguras porque no permiten el estrechamiento. B) Los brackers rizados son menos seguros porque pueden omitir constructores privados o eliminados, y llamar implícitamente a constructores marcados explícitamente.

Esos dos combinados significan que son más seguros si lo que hay dentro son constantes primitivas, pero menos seguros si son objetos (aunque se arreglan en C ++ 20)


Traté de buscar en goldbolt.org para omitir los constructores "explícitos" o "privados" utilizando el código de muestra proporcionado y haciendo que uno u otro sea privado o explícito, y fui recompensado con los errores de compilación apropiados. ¿Te gustaría respaldar eso con algún código de muestra?
Mark Storer

Esta es la solución para el problema propuesto para C ++ 20: open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf
Allan Jensen

1
Si edita su respuesta para mostrar de qué versión (s) de C ++ está hablando, me complacería cambiar mi voto.
Mark Storer
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.