web-dev-qa-db-fra.com

Quand un constructeur privé n'est-il pas un constructeur privé?

Disons que j'ai un type et que je veux rendre son constructeur par défaut privé. J'écris ce qui suit:

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
}

Génial.

Mais ensuite, le constructeur s'avère ne pas être aussi privé que je le pensais:

class C {
    C() = default;
};

int main() {
    C c{};         // OK on all compilers
    auto c2 = C{}; // OK on all compilers
}    

Cela me semble être un comportement très surprenant, inattendu et explicitement indésirable. Pourquoi est-ce OK?

84
Barry

L'astuce est en C++ 14 8.4.2/5 [dcl.fct.def.default]:

... Une fonction est fournie par l'utilisateur si elle est déclarée par l'utilisateur et non explicitement par défaut ou supprimée lors de sa première déclaration. ...

Ce qui signifie que le constructeur par défaut de C est en fait pas fourni par l'utilisateur, car il était explicitement par défaut lors de sa première déclaration. En tant que tel, C n'a pas de constructeurs fournis par l'utilisateur et est donc un agrégat par 8.5.1/1 [dcl.init.aggr]:

Un agrégat est un tableau ou une classe (article 9) sans constructeur fourni par l'utilisateur (12.1), sans membre de données non statique privé ou protégé ( Article 11), aucune classe de base (article 10) et aucune fonction virtuelle (10.3).

57
Angew

Vous n'appelez pas le constructeur par défaut, vous utilisez l'initialisation d'agrégat sur un type d'agrégat. Les types d'agrégats peuvent avoir un constructeur par défaut, tant qu'il est par défaut là où il a été déclaré pour la première fois:

De [dcl.init.aggr]/1 :

Un agrégat est un tableau ou une classe (Clause [classe]) avec

  • aucun constructeur fourni par l'utilisateur ([class.ctor]) (y compris ceux hérités ([namespace.udecl]) d'une classe de base),
  • aucun membre de données non statique privé ou protégé (Clause [class.access]),
  • aucune fonction virtuelle ([class.virtual]), et
  • aucune classe de base virtuelle, privée ou protégée ([class.mi]).

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

Les fonctions par défaut explicites et les fonctions déclarées implicitement sont appelées collectivement fonctions par défaut, et l'implémentation doit leur fournir des définitions implicites ([class.ctor] [class.dtor], [class.copy]), ce qui pourrait signifier les définir comme supprimées . Une fonction est fournie par l'utilisateur si elle est déclarée par l'utilisateur et non explicitement par défaut ou supprimée lors de sa première déclaration. Une fonction par défaut explicitement fournie par l'utilisateur ( c'est-à-dire explicitement par défaut après sa première déclaration) est défini au point où il est explicitement par défaut; si une telle fonction est implicitement définie comme supprimée, le programme est mal formé. [Remarque: La déclaration d'une fonction par défaut après sa première déclaration peut fournir une exécution efficace et une définition concise tout en permettant une interface binaire stable à une base de code en évolution. - note de fin]

Ainsi, nos exigences pour un agrégat sont:

  • aucun membre non public
  • pas de fonctions virtuelles
  • pas de classes de base virtuelles ou non publiques
  • aucun constructeur fourni par l'utilisateur hérité ou autre, qui autorise uniquement les constructeurs qui sont:
    • déclarée implicitement, ou
    • déclaré et défini explicitement comme étant par défaut en même temps.

C remplit toutes ces conditions.

Naturellement, vous pouvez vous débarrasser de ce faux comportement de construction par défaut en fournissant simplement un constructeur par défaut vide, ou en définissant le constructeur par défaut après l'avoir déclaré:

class C {
    C(){}
};
// --or--
class C {
    C();
};
inline C::C() = default;
53
jaggedSpire

Angew's et jaggedSpire's ' les réponses sont excellentes et s'appliquent à c ++ 11 . Et c ++ 14 . Et c ++ 17 .

Cependant, dans c ++ 2 , les choses changent un peu et l'exemple de l'OP ne compilera plus:

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

Comme le soulignent les deux réponses, la raison pour laquelle les deux dernières déclarations fonctionnent est que C est un agrégat et c'est l'initialisation d'agrégat. Cependant, en raison de P1008 (en utilisant un exemple motivant pas trop différent de l'OP), la définition des changements d'agrégat en C++ 20 à, de [dcl.init.aggr ]/1 :

Un agrégat est un tableau ou une classe ([classe]) avec

  • non constructeurs déclarés par l'utilisateur ou hérités ([class.ctor]),
  • aucun membre de données non statique direct privé ou protégé ([class.access]),
  • aucune fonction virtuelle ([class.virtual]), et
  • aucune classe de base virtuelle, privée ou protégée ([class.mi]).

Je souligne. Maintenant, l'exigence n'est pas de constructeurs déclarés par l'utilisateur, alors qu'elle l'était (comme les deux utilisateurs citent dans leurs réponses et peuvent être consultés historiquement pour C++ 11 , C++ 14 , et C++ 17 ) non fourni par l'utilisateur constructeurs. Le constructeur par défaut de C est déclaré par l'utilisateur, mais pas fourni par l'utilisateur, et cesse donc d'être un agrégat en C++ 20.


Voici un autre exemple illustratif de modifications globales:

class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};

B n'était pas un agrégat en C++ 11 ou C++ 14 car il a une classe de base. Par conséquent, B{} appelle juste le constructeur par défaut (déclaré par l'utilisateur mais non fourni par l'utilisateur), qui a accès au constructeur par défaut protégé de A.

En C++ 17, suite à P0017 , les agrégats ont été étendus pour permettre les classes de base. B est un agrégat en C++ 17, ce qui signifie que B{} est une initialisation d'agrégat qui doit initialiser tous les sous-objets - y compris le sous-objet A. Mais parce que le constructeur par défaut de A est protégé, nous n'y avons pas accès, donc cette initialisation est mal formée.

En C++ 20, en raison du constructeur déclaré par l'utilisateur de B, il cesse à nouveau d'être un agrégat, donc B{} revient à invoquer le constructeur par défaut et c'est là encore une initialisation bien formée.

1
Barry