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?


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 )


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;

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.