¿Por qué los inicializadores de miembros no pueden usar paréntesis?

7 minutos de lectura

avatar de usuario de delphifirst
delphifirst

Por ejemplo, no puedo escribir esto:

class A
{
    vector<int> v(12, 1);
};

Solo puedo escribir esto:

class A
{
    vector<int> v1{ 12, 1 };
    vector<int> v2 = vector<int>(12, 1);
};

¿Por qué hay una diferencia entre estas dos sintaxis de declaración?

  • El primero llama a un constructor vector cuyas entradas son 12 y 1. El último llama a un constructor vector cuya entrada es una lista de inicializadores. Son fundamentalmente diferentes.

    – druckermanly

    19 de julio de 2014 a las 3:50

  • La razón de la cita estándar es porque la gramática es declarator corchete-o-igual-inicializador(opt)

    – chris

    19 de julio de 2014 a las 3:51

El fundamento de esta elección se menciona explícitamente en el documento relacionado. propuesta por inicializadores de miembros de datos no estáticos :

Un problema planteado en Kona con respecto al alcance de los identificadores:

Durante la discusión en el Grupo de trabajo central en la reunión de septiembre de 2007 en Kona, surgió una pregunta sobre el alcance de los identificadores en el inicializador. ¿Queremos permitir el alcance de la clase con la posibilidad de búsqueda directa? ¿O queremos exigir que los inicializadores estén bien definidos en el punto en que se analizan?

Lo que se desea:

La motivación para la búsqueda de alcance de clase es que nos gustaría poder poner cualquier cosa en el inicializador de un miembro de datos no estáticos que podamos poner en un inicializador de mem sin cambiar significativamente la semántica (inicialización directa de módulo frente a inicialización de copia) :

int x();

struct S {
    int i;
    S() : i(x()) {} // currently well-formed, uses S::x()
    // ...
    static int x();
};

struct T {
    int i = x(); // should use T::x(), ::x() would be a surprise
    // ...
    static int x();
};

Problema 1:

Desafortunadamente, esto hace que los inicializadores de la forma “(expression-list)” sean ambiguos en el momento en que se analiza la declaración:

   struct S {
        int i(x); // data member with initializer
        // ...
        static int x;
    };

    struct T {
        int i(x); // member function declaration
        // ...
        typedef int x;
    };

Una posible solución es confiar en la regla existente de que, si una declaración puede ser un objeto o una función, entonces es una función:

 struct S {
        int i(j); // ill-formed...parsed as a member function,
                  // type j looked up but not found
        // ...
        static int j;
    };

Una solución similar sería aplicar otra regla existente, actualmente utilizada solo en plantillas, que si T podría ser un tipo o algo más, entonces es algo más; y podemos usar “typename” si realmente nos referimos a un tipo:

struct S {
        int i(x); // unabmiguously a data member
        int j(typename y); // unabmiguously a member function
    };

Ambas soluciones introducen sutilezas que probablemente muchos usuarios malinterpreten (como lo demuestran las muchas preguntas en comp.lang.c++ sobre por qué “int i();” en el alcance del bloque no declara un int inicializado por defecto) .

La solución propuesta en este documento es permitir solo los inicializadores de las formas “= cláusula-inicializador” y “{lista-inicializadores}”.. Eso resuelve el problema de la ambigüedad en la mayoría casos, por ejemplo:

HashingFunction hash_algorithm{"MD5"};

Aquí, no pudimos usar el formulario = porque el constructor de HasningFunction es explícito. En casos especialmente complicados, es posible que un tipo deba mencionarse dos veces. Considerar:

   vector<int> x = 3; // error:  the constructor taking an int is explicit
   vector<int> x(3);  // three elements default-initialized
   vector<int> x{3};  // one element with the value 3

En ese caso, tenemos que elegir entre las dos alternativas usando la notación apropiada:

vector<int> x = vector<int>(3); // rather than vector<int> x(3);
vector<int> x{3}; // one element with the value 3

Problema 2:

Otro problema es que, debido a que no proponemos ningún cambio en las reglas para inicializar miembros de datos estáticos, agregar la palabra clave static podría hacer que un inicializador bien formado tenga una forma incorrecta:

   struct S {
               const int i = f(); // well-formed with forward lookup
        static const int j = f(); // always ill-formed for statics
        // ...
        constexpr static int f() { return 0; }
    };

Problema 3:

Un tercer problema es que la búsqueda de ámbito de clase podría convertir un error de tiempo de compilación en un error de tiempo de ejecución:

struct S {
    int i = j; // ill-formed without forward lookup, undefined behavior with
    int j = 3;
};

(A menos que el compilador lo detecte, es posible que se inicialice con el valor indefinido de j).

La propuesta:

CWG tuvo una encuesta de opinión de 6 a 3 en Kona a favor de la búsqueda de alcance de clase; y eso es lo que propone este artículo, con inicializadores para miembros de datos no estáticos limitados a las formas “= cláusula-inicializador” y “{lista-inicializadores}”.

Creemos:

Problema 1: Este problema no ocurre ya que no proponemos la notación (). Las notaciones de inicialización = y {} no sufren este problema.

Problema 2: agregar la palabra clave static hace una serie de diferencias, siendo esta la menor de ellas.

Problema 3: este no es un problema nuevo, pero es el mismo problema de orden de inicialización que ya existe con los inicializadores del constructor.

  • +1 por desenterrar eso y formatear para SO.

    – Saludos y hth. – alf

    19 de julio de 2014 a las 6:32

Una posible razón es que permitir los paréntesis nos llevaría de vuelta a la análisis más irritante al instante. Considere los dos tipos siguientes:

struct foo {};
struct bar
{
  bar(foo const&) {}
};

Ahora, tiene un miembro de datos de tipo bar que desea inicializar, por lo que lo define como

struct A
{
  bar B(foo());
};

Pero lo que has hecho arriba es declarar una función llamada B que devuelve un bar objeto por valor, y toma un solo argumento que es una función que tiene la firma foo() (devuelve un foo y no admite argumentos).

A juzgar por la cantidad y la frecuencia de las preguntas que se hacen en StackOverflow sobre este problema, esto es algo que la mayoría de los programadores de C++ encuentran sorprendente y poco intuitivo. Agregando el nuevo inicializador de llave o igual la sintaxis fue una oportunidad para evitar esta ambigüedad y comenzar de cero, que es probablemente la razón por la que el comité de C++ decidió hacerlo.

bar B{foo{}};
bar B = foo();

Las dos líneas anteriores declaran un objeto llamado B de tipo barcomo se esperaba.


Aparte de las conjeturas anteriores, me gustaría señalar que estás haciendo dos cosas muy diferentes en tu ejemplo anterior.

vector<int> v1{ 12, 1 };
vector<int> v2 = vector<int>(12, 1);

La primera línea se inicializa v1 a un vector que contiene dos elementos, 12 y 1. El segundo crea un vector. v2 eso contiene 12 elementos, cada uno inicializado para 1.

Tenga cuidado con esta regla: si un tipo define un constructor que toma un initializer_list<T>entonces ese constructor es siempre considerado primero cuando el inicializador para el tipo es un braced-init-list. Los demás constructores serán considerados sólo si el que toma la initializer_list no es viable

  • Cuando se utiliza en la declaración de parámetros, foo() es un puntero de función que no es una función en sí misma, tal como lo hace una declaración de matriz integrada.

    – Lingxi

    19 de julio de 2014 a las 5:34

  • @Lingxi ¿No es eso lo que he dicho yo también?

    – Pretoriano

    19 de julio de 2014 a las 5:37

  • Creo que la lógica no puede guiar de manera confiable sobre pequeños detalles de C ++. Por ejemplo, lógicamente, dado que la inicialización de la lista se puede escribir v1{{12, 1}}el significado de v1{12,1} podría elegirse para admitir la llamada de constructor ordinaria. Esa sería mi elección como diseñador a partir de “borrón y cuenta nueva” aquí. 😉

    – Saludos y hth. – alf

    19 de julio de 2014 a las 5:38


  • @Praetorian En su declaración original, me suena un poco como una referencia a la función. No es un gran problema, de verdad.

    – Lingxi

    19 de julio de 2014 a las 5:42

  • ¿Cómo es esto peor que el análisis más desconcertante que aparece en otros lugares?

    – Ben Voigt

    19 de julio de 2014 a las 5:43

¿Ha sido útil esta solución?