¿Cuándo un constructor privado no es un constructor privado?

92

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?

Barry
fuente
25
¿No es C c{};la inicialización agregada, por lo que no se llama a ningún constructor?
NathanOliver
5
Lo que dijo @NathanOliver. No tiene un constructor proporcionado por el usuario, por lo que Ces un agregado.
Kerrek SB
5
@KerrekSB Al mismo tiempo, me sorprendió bastante que el usuario que declara explícitamente un ctor no lo hace proporcionado por el usuario.
Angew ya no está orgulloso de SO
1
@Angew Por eso estamos todos aquí :)
Barry
2
@Angew Si fuera un =defaultctor público , parecería más razonable. Pero el =defaultctor privado parece algo importante que no debe ignorarse. Además, class C { C(); } inline C::C()=default;ser bastante diferente es algo sorprendente.
Yakk - Adam Nevraumont

Respuestas:

61

El truco está en C ++ 14 8.4.2 / 5 [dcl.fct.def.default]:

... Una función es proporcionada por el usuario si está declarada por el usuario y no está explícitamente predeterminada o eliminada en su primera declaración. ...

Lo que significa que Cel constructor predeterminado en realidad no es proporcionado por el usuario, porque fue explícitamente predeterminado en su primera declaración. Como tal, Cno tiene constructores proporcionados por el usuario y, por lo tanto, es un agregado por 8.5.1 / 1 [dcl.init.aggr]:

Un agregado es una matriz o una clase (Cláusula 9) sin constructores proporcionados por el usuario (12.1), sin miembros de datos no estáticos privados o protegidos (Cláusula 11), sin clases base (Cláusula 10) y sin funciones virtuales (10.3 ).

Angew ya no está orgulloso de SO
fuente
13
En efecto, un pequeño defecto estándar: el hecho de que el ctor predeterminado fuera privado se ignora en este contexto.
Yakk - Adam Nevraumont
2
@Yakk No me siento calificado para juzgar eso. Sin embargo, la redacción de que el ctor no es proporcionada por el usuario parece muy deliberada.
Angew ya no está orgulloso de SO
1
@Yakk: Bueno, sí y no. Si la clase tuviera miembros de datos, tendrías la oportunidad de hacerlos privados. Sin miembros de datos, hay muy pocas situaciones en las que esta situación afectaría seriamente a alguien.
Kerrek SB
2
@KerrekSB Importa si estás intentando usar la clase como una especie de "token de acceso", controlando, por ejemplo, quién puede llamar a una función en función de quién puede crear un objeto de la clase.
Angew ya no está orgulloso de SO
5
@Yakk Aún más interesante es que C{}funciona incluso si el constructor es deleted.
Barry
56

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 :

Un agregado es una matriz o una clase (Cláusula [clase]) con

  • sin constructores proporcionados por el usuario ([class.ctor]) (incluidos los heredados ([namespace.udecl]) de una clase base),
  • sin miembros de datos no estáticos privados o protegidos (cláusula [class.access]),
  • sin funciones virtuales ([class.virtual]), y
  • sin clases base virtuales, privadas o protegidas ([class.mi]).

y de [dcl.fct.def.default] / 5

Las funciones explícitamente predeterminadas y las funciones declaradas implícitamente se denominan colectivamente funciones predeterminadas, y la implementación proporcionará definiciones implícitas para ellas ([class.ctor] [class.dtor], [class.copy]), lo que podría significar definirlas como eliminadas . Una función es proporcionada por el usuario si está declarada por el usuario y no está explícitamente predeterminada o eliminada en su primera declaración. Una función explícitamente predeterminada proporcionada por el usuario (es decir, explícitamente predeterminada después de su primera declaración) se define en el punto en el que está explícitamente predeterminada; si dicha función se define implícitamente como eliminada, el programa está mal formado.[Nota: Declarar una función como predeterminada después de su primera declaración puede proporcionar una ejecución eficiente y una definición concisa al tiempo que permite una interfaz binaria estable para una base de código en evolución. - nota final]

Por tanto, nuestros requisitos para un agregado son:

  • no miembros no públicos
  • sin funciones virtuales
  • sin clases base virtuales o no públicas
  • no hay constructores proporcionados por el usuario heredados o de otro tipo, lo que permite solo constructores que son:
    • declarado implícitamente, o
    • explícitamente declarado y definido como predeterminado al mismo tiempo.

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;
jaggedSpire
fuente
2
Me gusta esta respuesta algo más que la de Angew, pero creo que se beneficiaría de un resumen al principio en dos oraciones como máximo.
PJTraill
7

Las respuestas de Angew y jaggedSpire son excelentes y se aplican a. Y. Y.

Sin embargo, en , 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 Ces 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 :

Un agregado es una matriz o una clase ([clase]) con

  • sin constructores heredados o declarados por el usuario ([class.ctor]),
  • sin miembros de datos no estáticos directos privados o protegidos ([class.access]),
  • sin funciones virtuales ([class.virtual]), y
  • sin clases base virtuales, privadas o protegidas ([class.mi]).

É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 Ces 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{};

Bno 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 al Aconstructor predeterminado protegido.

En C ++ 17, como resultado de P0017 , los agregados se ampliaron para permitir clases base. Bes un agregado en C ++ 17, lo que significa que B{}es una inicialización agregada que tiene que inicializar todos los subobjetos, incluido el Asubobjeto. Pero debido a que Ael constructor predeterminado está protegido, no tenemos acceso a él, por lo que esta inicialización está mal formada.

En C ++ 20, debido al Bconstructor declarado por el usuario, vuelve a dejar de ser un agregado, por lo que B{}vuelve a invocar el constructor predeterminado y esta es una inicialización bien formada.

Barry
fuente