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

88

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 alors, 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 correct?

Barry
la source
24
L' C c{};initialisation de l'agrégat n'est-elle pas donc aucun constructeur n'est appelé?
NathanOliver
5
Ce que @NathanOliver a dit. Vous n'avez pas de constructeur fourni par l'utilisateur, tout Ccomme un agrégat.
Kerrek SB
5
@KerrekSB En même temps, il m'a été assez surprenant que l'utilisateur déclarant explicitement un ctor ne fasse pas que ce ctor soit fourni par l'utilisateur.
Angew n'est plus fier de SO
1
@Angew C'est pourquoi nous sommes tous ici :)
Barry
2
@Angew Si c'était un =defaultctor public , cela semblerait plus raisonnable. Mais le =defaultctor privé semble être une chose importante qui ne doit pas être ignorée. De plus, class C { C(); } inline C::C()=default;être assez différent est quelque peu surprenant.
Yakk - Adam Nevraumont

Réponses:

58

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 n'est pas explicitement définie par défaut ou supprimée lors de sa première déclaration. ...

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

Un agrégat est un tableau ou une classe (Article 9) sans constructeurs fournis par l'utilisateur (12.1), sans membres de données non statiques privés ou protégés (Article 11), sans classes de base (Article 10) et sans fonctions virtuelles (10.3 ).

Angew n'est plus fier de SO
la source
13
En effet, un petit défaut standard: le fait que le ctor par défaut soit privé est en effet ignoré dans ce contexte.
Yakk - Adam Nevraumont
2
@Yakk Je ne me sens pas qualifié pour juger cela. Le libellé selon lequel le ctor n'est pas fourni par l'utilisateur semble cependant très délibéré.
Angew n'est plus fier de SO
1
@Yakk: Eh bien, oui et non. Si la classe avait des membres de données, vous auriez une chance de les rendre privés. Sans les données membres, il existe très peu de situations où cette situation affecterait sérieusement qui que ce soit.
Kerrek SB
2
@KerrekSB Cela compte si vous essayez d'utiliser la classe comme une sorte de "jeton d'accès", contrôlant par exemple qui peut appeler une fonction en fonction de qui peut créer un objet de la classe.
Angew n'est plus fier de SO
5
@Yakk Encore plus intéressant, c'est que cela C{}fonctionne même si le constructeur est deleted.
Barry
55

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 sont autorisés à avoir un constructeur par défaut, à condition qu'il soit défini par défaut là où il a été déclaré pour la première fois:

Depuis [dcl.init.aggr] / 1 :

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

  • pas de constructeurs fournis par l'utilisateur ([class.ctor]) (y compris ceux hérités ([namespace.udecl]) d'une classe de base),
  • pas de données membres non statiques privées ou protégées (Clause [class.access]),
  • pas de fonctions virtuelles ([class.virtual]), et
  • pas de classes de base virtuelles, privées ou protégées ([class.mi]).

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

Les fonctions explicitement par défaut et les fonctions implicitement déclarées sont collectivement appelées fonctions par défaut, et l'implémentation doit leur fournir des définitions implicites ([class.ctor] [class.dtor], [class.copy]), ce qui peut signifier les définir comme supprimées . Une fonction est fournie par l'utilisateur si elle est déclarée par l'utilisateur et n'est pas explicitement définie par défaut ou supprimée lors de sa première déclaration. Une fonction explicitement par défaut fournie par l'utilisateur (c'est-à-dire explicitement par défaut après sa première déclaration) est définie au point où elle est explicitement définie par défaut; si une telle fonction est implicitement définie comme supprimée, le programme est mal formé.[Remarque: déclarer une fonction comme étant 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:

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

C répond à toutes ces exigences.

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;
flèche dentelée
la source
2
J'aime cette réponse un peu mieux que celle d'Angew, mais je pense qu'elle bénéficierait d'un résumé au début en deux phrases au maximum.
PJTraill
7

Les réponses d' Angew et de JaggedSpire sont excellentes et s'appliquent à. Et. Et.

Cependant, dans , les choses changent un peu et l'exemple dans l'OP ne sera plus compilé:

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 indiqué par les deux réponses, la raison pour laquelle les deux dernières déclarations fonctionnent est parce qu'il Cs'agit d'un agrégat et qu'il s'agit d'une initialisation d'agrégat. Cependant, à la suite de P1008 (en utilisant un exemple motivant pas trop différent de l'OP), la définition de l'agrégat change dans C ++ 20 à, de [dcl.init.aggr] / 1 :

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

  • aucun constructeur déclaré ou hérité par l'utilisateur ([class.ctor]),
  • aucun membre de données non statique direct privé ou protégé ([class.access]),
  • pas de fonctions virtuelles ([class.virtual]), et
  • pas de classes de base virtuelles, privées ou protégées ([class.mi]).

Soulignez le mien. Désormais, il ne s'agit pas de constructeurs déclarés par l'utilisateur , alors qu'auparavant (comme les deux utilisateurs le citent dans leurs réponses et peuvent être consultés historiquement pour C ++ 11 , C ++ 14 et C ++ 17 ), aucun constructeur fourni par l'utilisateur . Le constructeur par défaut pour Cest déclaré par l'utilisateur, mais pas fourni par l'utilisateur, et cesse donc d'être un agrégat dans C ++ 20.


Voici un autre exemple illustratif de modifications globales:

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

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

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

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

Barry
la source