El estilo de codificación es en última instancia subjetivo, y es muy poco probable que se obtengan beneficios sustanciales de rendimiento. Pero esto es lo que diría que obtiene del uso liberal de la inicialización uniforme:
Minimiza los nombres de tipo redundantes
Considera lo siguiente:
vec3 GetValue()
{
return vec3(x, y, z);
}
¿Por qué necesito escribir vec3
dos veces? ¿Hay algún punto en eso? El compilador sabe bien y bien lo que devuelve la función. ¿Por qué no puedo simplemente decir, "llamar al constructor de lo que devuelvo con estos valores y devolverlo?" Con una inicialización uniforme, puedo:
vec3 GetValue()
{
return {x, y, z};
}
Todo funciona.
Aún mejor es para argumentos de función. Considera esto:
void DoSomething(const std::string &str);
DoSomething("A string.");
Eso funciona sin tener que escribir un nombre de tipo, porque std::string
sabe cómo construirse a partir de una forma const char*
implícita. Eso es genial. Pero, ¿qué pasa si esa cadena proviene, dice RapidXML. O una cuerda de Lua. Es decir, digamos que realmente conozco la longitud de la cuerda por adelantado. El std::string
constructor que toma un const char*
tendrá que tomar la longitud de la cadena si solo paso un const char*
.
Sin embargo, hay una sobrecarga que toma una longitud explícitamente. Pero para usarlo, que tendría que hacer esto: DoSomething(std::string(strValue, strLen))
. ¿Por qué tiene el nombre de tipo adicional allí? El compilador sabe cuál es el tipo. Al igual que con auto
, podemos evitar tener nombres de tipos adicionales:
DoSomething({strValue, strLen});
Simplemente funciona Sin nombres de tipo, sin problemas, nada. El compilador hace su trabajo, el código es más corto y todos están contentos.
Por supuesto, hay argumentos para afirmar que la primera versión ( DoSomething(std::string(strValue, strLen))
) es más legible. Es decir, es obvio lo que está sucediendo y quién está haciendo qué. Eso es cierto, hasta cierto punto; comprender el código uniforme basado en la inicialización requiere mirar el prototipo de la función. Esta es la misma razón por la que algunos dicen que nunca debe pasar parámetros por referencia no constante: para que pueda ver en el sitio de la llamada si se está modificando un valor.
Pero podría decirse lo mismo auto
; saber de lo que se obtiene auto v = GetSomething();
requiere mirar la definición de GetSomething
. Pero eso no ha dejado auto
de usarse con un abandono casi imprudente una vez que tienes acceso a él. Personalmente, creo que estará bien una vez que te acostumbres. Especialmente con un buen IDE.
Nunca obtengas el análisis más irritante
Aquí hay un código.
class Bar;
void Func()
{
int foo(Bar());
}
Pop quiz: ¿qué es foo
? Si respondió "una variable", está equivocado. En realidad, es el prototipo de una función que toma como parámetro una función que devuelve a Bar
, y el foo
valor de retorno de la función es un int.
Esto se llama "Parse más irritante" de C ++ porque no tiene absolutamente ningún sentido para un ser humano. Pero las reglas de C ++ requieren tristemente esto: si posiblemente puede interpretarse como un prototipo de función, entonces lo será . El problema es Bar()
; Esa podría ser una de dos cosas. Podría ser un tipo llamado Bar
, lo que significa que está creando un temporal. O podría ser una función que no toma parámetros y devuelve a Bar
.
La inicialización uniforme no puede interpretarse como un prototipo de función:
class Bar;
void Func()
{
int foo{Bar{}};
}
Bar{}
Siempre crea un temporal. int foo{...}
Siempre crea una variable.
Hay muchos casos en los que desea usar Typename()
pero simplemente no puede debido a las reglas de análisis de C ++. Con Typename{}
, no hay ambigüedad.
Razones para no
El único poder real que renuncias es la reducción. No puede inicializar un valor más pequeño con uno más grande con inicialización uniforme.
int val{5.2};
Eso no se compilará. Puede hacerlo con una inicialización anticuada, pero no con una inicialización uniforme.
Esto se hizo en parte para que las listas de inicializadores realmente funcionen. De lo contrario, habría muchos casos ambiguos con respecto a los tipos de listas de inicializadores.
Por supuesto, algunos podrían argumentar que dicho código merece no compilarse. Personalmente estoy de acuerdo; El estrechamiento es muy peligroso y puede conducir a un comportamiento desagradable. Probablemente sea mejor detectar esos problemas desde el principio en la etapa de compilación. Como mínimo, el estrechamiento sugiere que alguien no está pensando demasiado en el código.
Tenga en cuenta que los compiladores generalmente le advertirán sobre este tipo de cosas si su nivel de advertencia es alto. Entonces, realmente, todo esto hace que la advertencia se convierta en un error forzado. Algunos podrían decir que deberías estar haciendo eso de todos modos;)
Hay otra razón para no:
std::vector<int> v{100};
Que hace esto? Podría crear un vector<int>
con cien elementos construidos por defecto. O podría crear un vector<int>
con 1 elemento cuyo valor es 100
. Ambos son teóricamente posibles.
En realidad, hace lo último.
¿Por qué? Las listas de inicializador usan la misma sintaxis que la inicialización uniforme. Por lo tanto, debe haber algunas reglas para explicar qué hacer en caso de ambigüedad. La regla es bastante simple: si el compilador puede usar un constructor de lista de inicializador con una lista inicializada con llaves, entonces lo hará . Como vector<int>
tiene un constructor de lista de inicializadores que toma initializer_list<int>
, y {100} podría ser válido initializer_list<int>
, por lo tanto, debe serlo .
Para obtener el constructor de dimensionamiento, debe usar en ()
lugar de {}
.
Tenga en cuenta que si se tratara vector
de algo que no fuera convertible a un entero, esto no sucedería. Initializer_list no encajaría en el constructor de la lista de inicializadores de ese vector
tipo y, por lo tanto, el compilador podría elegir libremente entre los otros constructores.