¿La inicialización uniforme de C ++ 11 es un reemplazo para la sintaxis de estilo antiguo?

172

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.

void.pointer
fuente
Este podría ser un tema mejor discutido en Programmers.se. Parece inclinarse hacia el lado del buen subjetivo.
Nicol Bolas
66
@NicolBolas: Por otro lado, su excelente respuesta podría ser un muy buen candidato para la etiqueta c ++ - faq. No creo que hayamos explicado esto antes.
Matthieu M.

Respuestas:

233

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 vec3dos 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::stringsabe 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::stringconstructor 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 autode 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 foovalor 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 vectorde 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 vectortipo y, por lo tanto, el compilador podría elegir libremente entre los otros constructores.

Nicol Bolas
fuente
11
+1 lo clavó. Estoy borrando mi respuesta ya que la suya aborda todos los mismos puntos con mucho más detalle.
R. Martinho Fernandes
21
El último punto es por qué realmente me gustaría std::vector<int> v{100, std::reserve_tag};. De manera similar con std::resize_tag. Actualmente toma dos pasos para reservar espacio vectorial.
Xeo
66
@NicolBolas - Dos puntos: pensé que el problema con el análisis molesto era foo (), no Bar (). En otras palabras, si lo hiciera 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?
void.pointer
77
@RobertDailey: "si lo hicieras 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 que Bar()podría ser un nombre de tipo o un valor temporal. Eso es lo que crea la ambigüedad para el compilador.
Nicol Bolas
8
unpleasant behavior- Hay un nuevo término estándar para recordar:>
sehe
64

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:

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

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:

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

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:

void DoSomething(const CoolStringType& str);

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:

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

Ejemplo del uso de la noción # 2:

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

Creo que es malo que el nuevo estándar permita a las personas inicializar escaleras como esta:

Stairs s {2, 4, 6};

... 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:

T var1;
T var2 (var1);

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:

T var2 {var1}; // fails if T is std::array for example
TommiT
fuente
48
Si tiene "<montones y montones de código aquí>" su código será difícil de entender independientemente de la sintaxis.
Kevin Cline
8
Además de IMO, es deber de su IDE informarle qué tipo está devolviendo (por ejemplo, al pasar el mouse sobre él). Por supuesto, si usted no utiliza un IDE, se tomó la carga sobre sí mismo :)
abergmeier
44
@TommiT Estoy de acuerdo con algunas partes de lo que dices. Sin embargo, en el mismo espíritu que el debate sobre la declaración de tipo explícitoauto 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(....)
sehe
55
" Pero la sintaxis que usa llaves no funciona si el tipo T es un agregado: " Tenga en cuenta que este es un defecto reportado en el comportamiento estándar, más que intencional, esperado.
Nicol Bolas
55
"Ahora, tendrá que volver a la firma de la función" si tiene que desplazarse, su función es demasiado grande.
Miles Rout
-3

Si sus constructores merely copy their parametersen las respectivas variables de clase in exactly the same orderen 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
2
¿Por qué dices que puede ser más rápido?
jbcoe
Esto es incorrecto. No hay ningún requisito para declarar un constructor: 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.
Tim