¿Cuándo usar el inicializador entre corchetes?

94

En C ++ 11, tenemos esa nueva sintaxis para inicializar clases que nos da una gran cantidad de posibilidades de cómo inicializar variables.

{ // Example 1
  int b(1);
  int a{1};
  int c = 1;
  int d = {1};
}
{ // Example 2
  std::complex<double> b(3,4);
  std::complex<double> a{3,4};
  std::complex<double> c = {3,4};
  auto d = std::complex<double>(3,4);
  auto e = std::complex<double>{3,4};
}
{ // Example 3
  std::string a(3,'x');
  std::string b{3,'x'}; // oops
}
{ // Example 4
  std::function<int(int,int)> a(std::plus<int>());
  std::function<int(int,int)> b{std::plus<int>()};
}
{ // Example 5
  std::unique_ptr<int> a(new int(5));
  std::unique_ptr<int> b{new int(5)};
}
{ // Example 6
  std::locale::global(std::locale("")); // copied from 22.4.8.3
  std::locale::global(std::locale{""});
}
{ // Example 7
  std::default_random_engine a {}; // Stroustrup's FAQ
  std::default_random_engine b;
}
{ // Example 8
  duration<long> a = 5; // Stroustrup's FAQ too
  duration<long> b(5);
  duration<long> c {5};
}

Para cada variable que declaro, tengo que pensar qué sintaxis de inicialización debo usar y esto ralentiza mi velocidad de codificación. Estoy seguro de que esa no era la intención de introducir las llaves.

Cuando se trata de código de plantilla, cambiar la sintaxis puede dar lugar a significados diferentes, por lo que es esencial ir por el camino correcto.

Me pregunto si existe una pauta universal sobre qué sintaxis se debe elegir.

helami
fuente
1
Un ejemplo de comportamiento no deseado de {} inicialización: cadena (50, 'x') vs cadena {50, 'x'} aquí
P i

Respuestas:

64

Creo que lo siguiente podría ser una buena guía:

  • Si el valor (único) con el que está inicializando está destinado a ser el valor exacto del objeto, use la =inicialización copy ( ) (porque entonces, en caso de error, nunca invocará accidentalmente un constructor explícito, que generalmente interpreta el valor proporcionado diferentemente). En lugares donde la inicialización de copia no está disponible, vea si la inicialización de llaves tiene la semántica correcta, y si es así, úsela; de lo contrario, use la inicialización de paréntesis (si eso tampoco está disponible, de todos modos no tendrá suerte).

  • Si los valores con los que está inicializando son una lista de valores que se almacenarán en el objeto (como los elementos de un vector / matriz, o la parte real / imaginaria de un número complejo), use la inicialización con llaves si está disponible.

  • Si los valores con los que está inicializando no son valores para almacenar, pero describen el valor / estado previsto del objeto, utilice paréntesis. Algunos ejemplos son el argumento de tamaño de a vectoro el argumento de nombre de archivo de un fstream.

celtschk
fuente
4
@ user1304032: una configuración regional no es una cadena, por lo tanto, no usaría la inicialización de copia. Una configuración regional tampoco contiene una cadena (podría almacenar esa cadena como detalle de implementación, pero ese no es su propósito), por lo tanto, no usaría la inicialización de llaves. Por lo tanto, la guía dice que se use la inicialización de paréntesis.
celtschk
2
Personalmente, esta guía me gustó como máximo y también funciona bien en código genérico. Hay algunas excepciones ( T {}o razones sintácticas como el análisis más molesto ), pero en general creo que este es un buen consejo. Tenga en cuenta que esta es mi opinión subjetiva, por lo que también debería echar un vistazo a las otras respuestas.
helami
2
@celtschk: Eso no funcionará para tipos que no se pueden copiar ni mover; type var{};hace.
ildjarn
2
@celtschk: No estoy diciendo que sea algo que suceda con frecuencia, pero se escribe menos y funciona en más contextos, entonces, ¿cuál es la desventaja?
ildjarn
2
Ciertamente, mis pautas nunca exigen la inicialización de la copia. ; -]
ildjarn
26

Estoy bastante seguro de que nunca habrá una pauta universal. Mi enfoque es usar siempre llaves para recordar que

  1. Los constructores de la lista de inicializadores tienen prioridad sobre otros constructores
  2. Todos los contenedores de bibliotecas estándar y std :: basic_string tienen constructores de lista de inicializadores.
  3. La inicialización de llaves no permite reducir las conversiones.

Entonces, los tirantes redondos y rizados no son intercambiables. Pero saber en qué se diferencian me permite usar la inicialización de corchetes rizados sobre redondos en la mayoría de los casos (algunos de los casos en los que no puedo son errores de compilación).

juanchopanza
fuente
6
Las llaves tienen la desventaja de que puedo llamar al constructor de listas por error. Los corchetes redondos no lo hacen. ¿No es esa una razón para usar los corchetes por defecto?
helami
4
@usuario: int i = 0;No creo que nadie lo use int i{0}allí, y podría ser confuso (también, 0es si el tipo int, por lo que no habría un estrechamiento ). Para todo lo demás, seguiría el consejo de Juancho: prefiero {}, cuidado con los pocos casos en los que no debes. Tenga en cuenta que no hay muchos tipos que tomarán listas de inicializadores como argumentos de constructor, puede esperar que los contenedores y tipos similares a contenedores (tupla ...) los tengan, pero la mayoría del código llamará al constructor apropiado.
David Rodríguez - dribeas
3
@ user1304032, depende de si le importa el estrechamiento. Sí, así que prefiero que el compilador me diga que int i{some floating point}es un error, en lugar de truncarlo silenciosamente.
juanchopanza
3
Con respecto a "prefer {}, tenga cuidado con los pocos casos en los que no debería": digamos que dos clases tienen un constructor semánticamente equivalente pero una clase también tiene una lista de inicializadores. ¿Deberían llamarse de forma diferente los dos constructores equivalentes?
Helami
3
@helami: "Digamos que dos clases tienen un constructor semánticamente equivalente pero una clase también tiene una lista de inicializadores. ¿Deberían llamarse de manera diferente los dos constructores equivalentes?" Digamos que me encuentro con el análisis más irritante; eso puede suceder en cualquier constructor para cualquier instancia. Es mucho más fácil evitar esto si solo usa {}para decir "inicializar" a menos que no pueda en absoluto .
Nicol Bolas
16

Fuera del código genérico (es decir, plantillas), puede (y yo lo hago) usar llaves en todas partes . Una ventaja es que funciona en todas partes, por ejemplo, incluso para la inicialización en clase:

struct foo {
    // Ok
    std::string a = { "foo" };

    // Also ok
    std::string b { "bar" };

    // Not possible
    std::string c("qux");

    // For completeness this is possible
    std::string d = "baz";
};

o para argumentos de función:

void foo(std::pair<int, double*>);
foo({ 42, nullptr });
// Not possible with parentheses without spelling out the type:
foo(std::pair<int, double*>(42, nullptr));

Para las variables a las que no presto mucha atención entre los estilos T t = { init };o T t { init };, encuentro que la diferencia es menor y, en el peor de los casos, solo resultará en un mensaje útil del compilador sobre el mal uso de un explicitconstructor.

Para tipos que aceptan, std::initializer_listaunque obviamente, a veces std::initializer_listse necesitan los no constructores (siendo el ejemplo clásico std::vector<int> twenty_answers(20, 42);). Entonces está bien no usar frenillos.


Cuando se trata de código genérico (es decir, en plantillas), el último párrafo debería haber generado algunas advertencias. Considera lo siguiente:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T { std::forward<Args>(args)... } }; }

Luego auto p = make_unique<std::vector<T>>(20, T {});crea un vector de tamaño 2 si Tes int, por ejemplo , o un vector de tamaño 20 si Tes std::string. Una señal muy reveladora de que algo muy mal está sucediendo aquí es que no hay ningún rasgo que pueda salvarlo aquí (por ejemplo, con SFINAE): std::is_constructiblees en términos de inicialización directa, mientras que estamos usando inicialización de llaves, que difiere de la inicialización directa. inicialización si y solo si no hay ningún constructor que std::initializer_listinterfiera. Del mismo modo no std::is_convertiblees de ayuda.

He investigado si es posible aplicar un rasgo que pueda solucionarlo, pero no soy demasiado optimista al respecto. En cualquier caso no creo que nos falte mucho, creo que el hecho de que make_unique<T>(foo, bar)resulte en una construcción equivalente a T(foo, bar)es muy intuitivo; especialmente dado que make_unique<T>({ foo, bar })es bastante diferente y solo tiene sentido si fooy bartienen el mismo tipo.

Por lo tanto, para el código genérico, solo uso llaves para la inicialización de valores (por ejemplo, T t {};o T t = {};), lo cual es muy conveniente y creo que es superior a la forma C ++ 03 T t = T();. De lo contrario, es una sintaxis de inicialización directa (es decir T t(a0, a1, a2);) o, a veces, una construcción predeterminada ( T t; stream >> t;creo que es el único caso en el que lo uso).

Sin embargo, eso no significa que todas las llaves sean malas, considere el ejemplo anterior con correcciones:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T(std::forward<Args>(args)...) }; }

Esto todavía usa llaves para construir el std::unique_ptr<T>, aunque el tipo real depende del parámetro de la plantilla T.

Luc Danton
fuente
@interjay Es posible que algunos de mis ejemplos de hecho necesiten usar tipos sin firmar, por ejemplo, make_unique<T>(20u, T {})para Tser unsignedo std::string. No estoy muy seguro de los detalles. (Tenga en cuenta que también comenté sobre las expectativas con respecto a la inicialización directa frente a la inicialización de llaves con respecto, por ejemplo, a las funciones de reenvío perfecto). std::string c("qux");No se ha especificado que funcione como una inicialización en clase para evitar ambigüedades con las declaraciones de funciones miembro en la gramática.
Luc Danton
@interjay No estoy de acuerdo con usted en el primer punto, no dude en comprobar 8.5.4 Inicialización de lista y 13.3.1.7 Inicialización por inicialización de lista. En cuanto al segundo, debe observar más de cerca lo que escribí (que se refiere a la inicialización en clase ) y / o la gramática de C ++ (por ejemplo, miembro-declarator , que hace referencia a brace-or-equal-initializer ).
Luc Danton
Hmm, tienes razón: estaba probando con GCC 4.5 anteriormente, lo que parecía confirmar lo que estaba diciendo, pero GCC 4.6 está de acuerdo contigo. Y me perdí el hecho de que estabas hablando de inicialización en clase. Mis disculpas.
interjay