¿Por qué la inicialización agregada ya no funciona desde C++ 20 si un constructor está explícitamente predeterminado o eliminado?
c++17 backwards-compatibility (3)
Estoy migrando un proyecto de Visual Studio de C ++ de VS2017 a VS2019.
Ahora recibo un error, que no ocurrió antes, que se puede reproducir con estas pocas líneas de código:
struct Foo
{
Foo() = default;
int bar;
};
auto test = Foo { 0 };
El error es
(6): error C2440: ''inicializando'': no se puede convertir de ''lista de inicializadores'' a ''Foo''
(6): nota: ningún constructor podría tomar el tipo de fuente, o la resolución de sobrecarga del constructor era ambigua
El proyecto se compila con
/std:c++latest
indicador.
Lo reproduje en
godbolt
.
Si lo cambio a
/std:c++17
, se compila bien como antes.
Intenté compilar el mismo código con
clang
con
-std=c++2a
y obtuve un error similar.
Además, la omisión o eliminación de otros constructores genera este error.
Aparentemente, se agregaron algunas características nuevas de C ++ 20 en VS2019 y supongo que el origen de este problema se describe en https://en.cppreference.com/w/cpp/language/aggregate_initialization . Allí dice que un agregado puede ser una estructura que (entre otros criterios) tiene
- no hay constructores proporcionados por el usuario, heredados o explícitos (se permiten constructores explícitamente predeterminados o eliminados) (desde C ++ 17) (hasta C ++ 20)
- sin constructores heredados o declarados por el usuario (desde C ++ 20)
Tenga en cuenta que la parte entre paréntesis "se permiten constructores explícitamente predeterminados o eliminados" se eliminó y que "proporcionado por el usuario" cambió a "declarado por el usuario".
Entonces, mi primera pregunta es, ¿estoy en lo cierto al suponer que este cambio en el estándar es la razón por la cual mi código se compiló antes pero ya no?
Por supuesto, es fácil solucionar esto: simplemente elimine los constructores predeterminados explícitamente.
Sin embargo, he omitido y eliminado explícitamente muchos constructores en todos mis proyectos porque descubrí que era un buen hábito hacer que el código fuera mucho más expresivo de esta manera porque simplemente da como resultado menos sorpresas que con constructores omitidos o eliminados implícitamente. Sin embargo, con este cambio, esto ya no parece un buen hábito ...
Entonces mi pregunta real es: ¿Cuál es el razonamiento detrás de este cambio de C ++ 17 a C ++ 20? ¿Fue esta ruptura de compatibilidad con versiones anteriores hecha a propósito? ¿Hubo algún intercambio como "Ok, estamos rompiendo la compatibilidad con versiones anteriores aquí, pero es por un bien mayor"? ¿Qué es este bien mayor?
El resumen de P1008 , la propuesta que condujo al cambio:
C ++ actualmente permite que algunos tipos con constructores declarados por el usuario se inicialicen a través de la inicialización agregada, evitando esos constructores. El resultado es un código sorprendente, confuso y con errores. Este documento propone una solución que hace que la semántica de inicialización en C ++ sea más segura, más uniforme y más fácil de enseñar. También discutimos los cambios importantes que presenta esta solución.
Uno de los ejemplos que dan es el siguiente.
struct X { int i{4}; X() = default; }; int main() { X x1(3); // ill-formed - no matching c’tor X x2{3}; // compiles! }
Para mí, está bastante claro que los cambios propuestos valen la incompatibilidad hacia atrás que soportan.
Y de hecho, ya no parece ser una buena práctica para
= default
constructores
= default
agregados predeterminados.
En realidad, MSDN abordó su preocupación en el siguiente documento:
Especificación modificada del tipo agregado
En Visual Studio 2019, bajo / std: c ++ más reciente, una clase con cualquier constructor declarado por el usuario (por ejemplo, incluido un constructor declarado = predeterminado o = eliminar) no es un agregado. Anteriormente, solo los constructores proporcionados por el usuario descalificaban a una clase de ser un agregado. Este cambio impone restricciones adicionales sobre cómo se pueden inicializar dichos tipos.
El razonamiento de P1008 (PDF) se puede entender mejor desde dos direcciones:
- Si se sentó un programador de C ++ relativamente nuevo frente a una definición de clase y pregunta "¿es esto un agregado", sería correcto?
La concepción común de un agregado es "una clase sin constructores".
Si
Typename() = default;
está en una definición de clase, la mayoría de la gente verá que tiene un constructor.
Se comportará como el constructor predeterminado estándar, pero el tipo todavía tiene uno.
Esa es la concepción amplia de la idea de muchos usuarios.
Se supone que un agregado es una clase de datos puros, capaz de hacer que cualquier miembro asuma cualquier valor que se le dé. Desde esa perspectiva, no tiene por qué darle constructores de ningún tipo, incluso si los incumplió. Lo que nos lleva al siguiente razonamiento:
- Si mi clase cumple con los requisitos de un agregado, pero no quiero que sea un agregado, ¿cómo hago eso?
La respuesta más obvia sería
= default
el constructor predeterminado, porque probablemente soy alguien del grupo # 1.
Obviamente, eso no funciona.
Antes de C ++ 20, sus opciones son dar a la clase algún otro constructor o implementar una de las funciones especiales del miembro. Ninguna de estas opciones es aceptable, porque (por definición) no es algo que realmente necesite implementar; solo lo estás haciendo para que ocurran algunos efectos secundarios.
Después de C ++ 20, la respuesta obvia funciona.
Al cambiar las reglas de tal manera, hace visible la diferencia entre un agregado y un no agregado. Los agregados no tienen constructores; así que si quieres que un tipo sea un agregado, no le des constructores.
Ah, y aquí hay un hecho divertido: anterior a C ++ 20, este es un agregado:
class Agg
{
Agg() = default;
};
Tenga en cuenta que el constructor predeterminado es
privado
, por lo que solo las personas con acceso privado a
Agg
pueden llamarlo ... a menos que usen
Agg{}
, omitan el constructor y es perfectamente legal.
La intención clara de esta clase es crear una clase que se pueda copiar, pero solo puede obtener su construcción inicial de aquellos con acceso privado.
Esto permite el reenvío de controles de acceso, ya que solo el código que recibió un
Agg
puede invocar funciones que toman
Agg
como parámetro.
Y solo el código con acceso a
Agg
puede crear uno.
O al menos, así es como se supone que debe ser.
Ahora puede solucionar esto de manera más específica al decir que es un agregado si los constructores predeterminados / eliminados no se declaran públicamente. Pero eso se siente aún más congruente; a veces, una clase con un constructor visiblemente declarado es un agregado y a veces no lo es, dependiendo de dónde esté ese constructor visiblemente declarado.