Entiendo que la inicialización uniforme de C ++ 11 resuelve cierta ambigüedad sintáctica en el lenguaje, pero en muchas de las presentaciones de Bjarne Stroustrup (particularmente aquellas durante las charlas de GoingNative 2012), sus ejemplos usan principalmente esta sintaxis ahora cuando está construyendo objetos.
¿Se recomienda ahora usar una inicialización uniforme en todos los casos? ¿Cuál debería ser el enfoque general para esta nueva característica en cuanto a estilo de codificación y uso general? ¿Cuáles son algunas razones para no usarlo?
Tenga en cuenta que en mi opinión estoy pensando principalmente en la construcción de objetos como mi caso de uso, pero si hay otros escenarios a considerar, hágamelo saber.
Respuestas:
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:
¿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:Todo funciona.
Aún mejor es para argumentos de función. Considera esto:
Eso funciona sin tener que escribir un nombre de tipo, porque
std::string
sabe cómo construirse a partir de una formaconst 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. Elstd::string
constructor que toma unconst char*
tendrá que tomar la longitud de la cadena si solo paso unconst 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 conauto
, podemos evitar tener nombres de tipos adicionales: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 obtieneauto v = GetSomething();
requiere mirar la definición deGetSomething
. Pero eso no ha dejadoauto
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.
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 aBar
, y elfoo
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 llamadoBar
, lo que significa que está creando un temporal. O podría ser una función que no toma parámetros y devuelve aBar
.La inicialización uniforme no puede interpretarse como un prototipo de función:
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 ++. ConTypename{}
, 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.
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:
Que hace esto? Podría crear un
vector<int>
con cien elementos construidos por defecto. O podría crear unvector<int>
con 1 elemento cuyo valor es100
. 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 tomainitializer_list<int>
, y {100} podría ser válidoinitializer_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 esevector
tipo y, por lo tanto, el compilador podría elegir libremente entre los otros constructores.fuente
std::vector<int> v{100, std::reserve_tag};
. De manera similar constd::resize_tag
. Actualmente toma dos pasos para reservar espacio vectorial.int foo(10)
, ¿no se encontraría con el mismo problema? En segundo lugar, otra razón para no usarlo parece ser más un problema de sobre ingeniería, pero ¿qué pasa si construimos todos nuestros objetos usando{}
, pero un día más tarde agrego un constructor para las listas de inicialización? Ahora todas mis declaraciones de construcción se convierten en declaraciones de la lista de inicializadores. Parece muy frágil en términos de refactorización. ¿Algún comentario sobre esto?int foo(10)
, ¿no tendrías el mismo problema?" No. 10 es un literal entero, y un literal entero nunca puede ser un tipo de nombre. El análisis irritante proviene del hecho de queBar()
podría ser un nombre de tipo o un valor temporal. Eso es lo que crea la ambigüedad para el compilador.unpleasant behavior
- Hay un nuevo término estándar para recordar:>No voy a estar de acuerdo con la sección de respuestas de Nicol Bolas Minimiza los nombres de tipo redundantes . Debido a que el código se escribe una vez y se lee varias veces, deberíamos tratar de minimizar la cantidad de tiempo que lleva leer y comprender el código, no la cantidad de tiempo que lleva escribir el código. Tratar de minimizar simplemente escribir es tratar de optimizar lo incorrecto.
Ver el siguiente código:
Alguien que lea el código anterior por primera vez probablemente no entenderá la declaración de devolución de inmediato, porque para cuando llegue a esa línea, se habrá olvidado del tipo de devolución. Ahora, tendrá que volver a la firma de la función o usar alguna función IDE para ver el tipo de retorno y comprender completamente la declaración de retorno.
Y aquí nuevamente no es fácil para alguien que lee el código por primera vez entender lo que realmente se está construyendo:
El código anterior se romperá cuando alguien decida que DoSomething también debería admitir algún otro tipo de cadena, y agrega esta sobrecarga:
Si CoolStringType tiene un constructor que toma un const char * y un size_t (al igual que std :: string) la llamada a DoSomething ({strValue, strLen}) dará como resultado un error de ambigüedad.
Mi respuesta a la pregunta real:
No, la inicialización uniforme no debe considerarse como un reemplazo de la sintaxis del constructor de estilo antiguo.
Y mi razonamiento es el siguiente:
si dos declaraciones no tienen el mismo tipo de intención, no deberían tener el mismo aspecto. Hay dos tipos de nociones de inicialización de objetos:
1) Tome todos estos elementos y viértalos en este objeto que estoy inicializando.
2) Construya este objeto usando estos argumentos que proporcioné como guía.
Ejemplos del uso de la noción # 1:
Ejemplo del uso de la noción # 2:
Creo que es malo que el nuevo estándar permita a las personas inicializar escaleras como esta:
... porque eso ofusca el significado del constructor. Una inicialización como esa se parece a la noción # 1, pero no lo es. No está vertiendo tres valores diferentes de alturas de paso en el objeto s, aunque parezca que es así. Y también, lo que es más importante, si se ha publicado una implementación de la biblioteca de Stairs como la anterior y los programadores la han estado usando, y luego si el implementador de la biblioteca luego agrega un constructor initializer_list a Stairs, entonces todo el código que ha estado usando Stairs with Uniform Initialization La sintaxis se va a romper.
Creo que la comunidad C ++ debería aceptar una convención común sobre cómo se usa la Inicialización Uniforme, es decir, de manera uniforme en todas las inicializaciones, o, como sugiero fuertemente, separar estas dos nociones de inicialización y aclarar así la intención del programador al lector de el código.
DESPUÉS:
Aquí hay otra razón por la cual no debe pensar en la Inicialización Uniforme como un reemplazo de la sintaxis anterior, y por qué no puede usar la notación de llaves para todas las inicializaciones:
Digamos que su sintaxis preferida para hacer una copia es:
Ahora cree que debe reemplazar todas las inicializaciones con la nueva sintaxis de llaves para que pueda ser (y el código se verá) más consistente. Pero la sintaxis que usa llaves no funciona si el tipo T es un agregado:
fuente
auto
frente a , yo abogaría por un equilibrio: los inicializadores uniformes son bastante importantes en situaciones de metaprogramación de plantillas en las que el tipo suele ser bastante obvio de todos modos. Evitará repetir su maldita complicación para el encantamiento, por ejemplo, para plantillas de funciones simples en línea (me hizo llorar).-> decltype(....)
Si sus constructores
merely copy their parameters
en las respectivas variables de clasein exactly the same order
en las que se declaran dentro de la clase, el uso de una inicialización uniforme puede ser más rápido (pero también puede ser absolutamente idéntico) que llamar al constructor.Obviamente, esto no cambia el hecho de que siempre debe declarar el constructor.
fuente
struct X { int i; }; int main() { X x{42}; }
. También es incorrecto, que la inicialización uniforme puede ser más rápida que la inicialización de valores.