Quando um construtor privado não é um construtor privado?

88

Digamos que eu tenha um tipo e desejo que seu construtor padrão seja privado. Eu escrevo o seguinte:

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
}

Ótimo.

Mas então, o construtor acabou não sendo tão privado quanto eu pensava que era:

class C {
    C() = default;
};

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

Isso me parece um comportamento muito surpreendente, inesperado e explicitamente indesejado. Por que isso está OK?

Barry
fonte
24
Não é C c{};a inicialização de agregação, então nenhum construtor é chamado?
NathanOliver
5
O que @NathanOliver disse. Você não tem um construtor fornecido pelo usuário, então Cé um agregado.
Kerrek SB
5
@KerrekSB Ao mesmo tempo, foi bastante surpreendente para mim que o usuário declarando explicitamente um ctor não o torne fornecido pelo usuário.
Angew não está mais orgulhoso de SO
1
@Angew É por isso que estamos todos aqui :)
Barry
2
@Angew Se fosse um =defaultctor público , isso pareceria mais razoável. Mas o =defaultctor privado parece algo importante que não deve ser ignorado. Além do mais, class C { C(); } inline C::C()=default;ser bem diferente é um tanto surpreendente.
Yakk - Adam Nevraumont

Respostas:

58

O truque está em C ++ 14 8.4.2 / 5 [dcl.fct.def.default]:

... Uma função é fornecida pelo usuário se for declarada pelo usuário e não explicitamente padronizada ou excluída em sua primeira declaração. ...

O que significa que Co construtor padrão não é fornecido pelo usuário, porque foi explicitamente padronizado em sua primeira declaração. Como tal, Cnão tem construtores fornecidos pelo usuário e, portanto, é um agregado por 8.5.1 / 1 [dcl.init.aggr]:

Um agregado é uma matriz ou uma classe (Cláusula 9) sem construtores fornecidos pelo usuário (12.1), sem membros de dados não estáticos privados ou protegidos (Cláusula 11), sem classes de base (Cláusula 10) e sem funções virtuais (10.3 )

Angew não está mais orgulhoso de SO
fonte
13
Na verdade, um pequeno defeito padrão: o fato de que o ctor padrão era privado é, com efeito, ignorado neste contexto.
Yakk - Adam Nevraumont
2
@Yakk Não me sinto qualificado para julgar isso. O texto sobre o ctor não ser fornecido pelo usuário parece muito deliberado, no entanto.
Angew não está mais orgulhoso de SO
1
@Yakk: Bem, sim e não. Se a classe tivesse quaisquer membros de dados, você teria a chance de torná-los privados. Sem membros de dados, existem muito poucas situações em que essa situação afetaria seriamente alguém.
Kerrek SB
2
@KerrekSB É importante se você está tentando usar a classe como uma espécie de "token de acesso", controlando, por exemplo, quem pode chamar uma função com base em quem pode criar um objeto da classe.
Angew não está mais orgulhoso de SO
5
@Yakk Ainda mais interessante é que C{}funciona mesmo se o construtor for deleted.
Barry
55

Você não está chamando o construtor padrão, está usando a inicialização de agregação em um tipo de agregação. Os tipos agregados podem ter um construtor padrão, contanto que seja padronizado onde for declarado pela primeira vez:

De [dcl.init.aggr] / 1 :

Um agregado é uma matriz ou uma classe (Cláusula [classe]) com

  • nenhum construtor fornecido pelo usuário ([class.ctor]) (incluindo aqueles herdados ([namespace.udecl]) de uma classe base),
  • nenhum membro de dados não estáticos privados ou protegidos (Cláusula [class.access]),
  • sem funções virtuais ([class.virtual]), e
  • nenhuma classe base virtual, privada ou protegida ([class.mi]).

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

Funções explicitamente padrão e funções declaradas implicitamente são chamadas coletivamente de funções padrão, e a implementação deve fornecer definições implícitas para elas ([class.ctor] [class.dtor], [class.copy]), o que pode significar defini-las como excluídas . Uma função é fornecida pelo usuário se for declarada pelo usuário e não explicitamente padronizada ou excluída em sua primeira declaração. Uma função explicitamente padronizada fornecida pelo usuário (ou seja, explicitamente padronizada após sua primeira declaração) é definida no ponto em que é explicitamente padronizada; se tal função for implicitamente definida como excluída, o programa está malformado.[Observação: declarar uma função como padrão após sua primeira declaração pode fornecer uma execução eficiente e uma definição concisa, ao mesmo tempo que permite uma interface binária estável para uma base de código em evolução. - nota final]

Assim, nossos requisitos para um agregado são:

  • nenhum membro não público
  • sem funções virtuais
  • nenhuma classe base virtual ou privada
  • nenhum construtor fornecido pelo usuário herdado ou não, o que permite apenas construtores que são:
    • declarado implicitamente, ou
    • declarado explicitamente e definido como padrão ao mesmo tempo.

C cumpre todos esses requisitos.

Naturalmente, você pode se livrar desse falso comportamento de construção padrão simplesmente fornecendo um construtor padrão vazio ou definindo o construtor como padrão após declará-lo:

class C {
    C(){}
};
// --or--
class C {
    C();
};
inline C::C() = default;
jaggedSpire
fonte
2
Gosto desta resposta um pouco mais do que a de Angew, mas acho que se beneficiaria de um resumo no início em no máximo duas frases.
PJTraill de
7

De Angew e jaggedSpire's ' respostas de são excelentes e se aplicam a. E. E.

No entanto, em , as coisas mudam um pouco e o exemplo no OP não compilará mais:

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

Conforme apontado pelas duas respostas, o motivo pelo qual as duas últimas declarações funcionam é porque Cé um agregado e esta é uma inicialização de agregado. No entanto, como resultado de P1008 (usando um exemplo motivador não muito diferente do OP), a definição de alterações agregadas em C ++ 20 para, de [dcl.init.aggr] / 1 :

Um agregado é uma matriz ou uma classe ([classe]) com

  • nenhum construtor declarado pelo usuário ou herdado ([class.ctor]),
  • sem membros de dados não estáticos diretos privados ou protegidos ([class.access]),
  • sem funções virtuais ([class.virtual]), e
  • nenhuma classe base virtual, privada ou protegida ([class.mi]).

Ênfase minha. Agora, o requisito não é nenhum construtor declarado pelo usuário , ao passo que costumava ser (como ambos os usuários citam em suas respostas e pode ser visto historicamente para C ++ 11 , C ++ 14 e C ++ 17 ) nenhum construtor fornecido pelo usuário . O construtor padrão para Cé declarado pelo usuário, mas não fornecido pelo usuário e, portanto, deixa de ser um agregado em C ++ 20.


Aqui está outro exemplo ilustrativo de alterações agregadas:

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

Bnão era um agregado em C ++ 11 ou C ++ 14 porque tem uma classe base. Como um resultado,B{} apenas invoca o construtor padrão (declarado pelo usuário, mas não fornecido pelo usuário), que tem acesso ao Aconstrutor padrão protegido de.

Em C ++ 17, como resultado de P0017 , os agregados foram estendidos para permitir classes básicas. Bé um agregado em C ++ 17, o que significa que B{}é uma inicialização de agregação que deve inicializar todos os subobjetos - incluindo o Asubobjeto. Mas, como Ao construtor padrão de é protegido, não temos acesso a ele, então essa inicialização está malformada.

No C ++ 20, por causa do Bconstrutor declarado pelo usuário, ele novamente deixa de ser uma agregação, então B{}volta a invocar o construtor padrão e esta é novamente uma inicialização bem formada.

Barry
fonte