Digamos que tengo un tipo y quiero que su constructor predeterminado sea privado. Escribo lo siguiente:
class C {
C() = default;
};
int main() {
C c; // error: C::C() is private within this context (g++)
// error: calling a private constructor of class 'C' (clang++)
// error C2248: 'C::C' cannot access private member declared in class 'C' (MSVC)
auto c2 = C(); // error: as above
}
Excelente.
Pero luego, el constructor resulta no ser tan privado como pensé que era:
class C {
C() = default;
};
int main() {
C c{}; // OK on all compilers
auto c2 = C{}; // OK on all compilers
}
Esto me parece un comportamiento muy sorprendente, inesperado y explícitamente no deseado. ¿Por qué está bien esto?
C c{};
la inicialización agregada, por lo que no se llama a ningún constructor?C
es un agregado.=default
ctor público , parecería más razonable. Pero el=default
ctor privado parece algo importante que no debe ignorarse. Además,class C { C(); } inline C::C()=default;
ser bastante diferente es algo sorprendente.Respuestas:
El truco está en C ++ 14 8.4.2 / 5 [dcl.fct.def.default]:
Lo que significa que
C
el constructor predeterminado en realidad no es proporcionado por el usuario, porque fue explícitamente predeterminado en su primera declaración. Como tal,C
no tiene constructores proporcionados por el usuario y, por lo tanto, es un agregado por 8.5.1 / 1 [dcl.init.aggr]:fuente
C{}
funciona incluso si el constructor esdelete
d.No está llamando al constructor predeterminado, está usando la inicialización agregada en un tipo agregado. Los tipos agregados pueden tener un constructor predeterminado, siempre y cuando esté predeterminado donde se declaró por primera vez:
Desde [dcl.init.aggr] / 1 :
y de [dcl.fct.def.default] / 5
Por tanto, nuestros requisitos para un agregado son:
C
cumple todos estos requisitos.Naturalmente, puede deshacerse de este comportamiento de construcción predeterminado falso simplemente proporcionando un constructor predeterminado vacío, o definiendo el constructor como predeterminado después de declararlo:
class C { C(){} }; // --or-- class C { C(); }; inline C::C() = default;
fuente
Las respuestas de Angew y jaggedSpire son excelentes y se aplican ac ++ 11. Yc ++ 14. Yc ++ 17.
Sin embargo, en c ++ 20, las cosas cambian un poco y el ejemplo en el OP ya no se compilará:
class C { C() = default; }; C p; // always error auto q = C(); // always error C r{}; // ok on C++11 thru C++17, error on C++20 auto s = C{}; // ok on C++11 thru C++17, error on C++20
Como se señala en las dos respuestas, la razón por la que las dos últimas declaraciones funcionan es porque
C
es un agregado y esto es una inicialización agregada. Sin embargo, como resultado de P1008 (usando un ejemplo motivador no muy diferente del OP), la definición de agregado cambia en C ++ 20 a, desde [dcl.init.aggr] / 1 :Énfasis mío. Ahora el requisito no es ningún constructor declarado por el usuario , mientras que solía ser (como ambos usuarios citan en sus respuestas y se puede ver históricamente para C ++ 11 , C ++ 14 y C ++ 17 ) sin constructores proporcionados por el usuario . El constructor predeterminado para
C
es declarado por el usuario, pero no proporcionado por el usuario, y por lo tanto deja de ser un agregado en C ++ 20.Aquí hay otro ejemplo ilustrativo de cambios agregados:
class A { protected: A() { }; }; struct B : A { B() = default; }; auto x = B{};
B
no era un agregado en C ++ 11 o C ++ 14 porque tiene una clase base. Como resultado,B{}
simplemente invoca el constructor predeterminado (declarado por el usuario pero no proporcionado por el usuario), que tiene acceso alA
constructor predeterminado protegido.En C ++ 17, como resultado de P0017 , los agregados se ampliaron para permitir clases base.
B
es un agregado en C ++ 17, lo que significa queB{}
es una inicialización agregada que tiene que inicializar todos los subobjetos, incluido elA
subobjeto. Pero debido a queA
el constructor predeterminado está protegido, no tenemos acceso a él, por lo que esta inicialización está mal formada.En C ++ 20, debido al
B
constructor declarado por el usuario, vuelve a dejar de ser un agregado, por lo queB{}
vuelve a invocar el constructor predeterminado y esta es una inicialización bien formada.fuente