Kiedy prywatny konstruktor nie jest prywatnym konstruktorem?

92

Powiedzmy, że mam typ i chcę ustawić jego domyślny konstruktor jako prywatny. Piszę co następuje:

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
}

Świetny.

Ale potem konstruktor okazuje się nie być tak prywatny, jak myślałem:

class C {
    C() = default;
};

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

Wydaje mi się to bardzo zaskakujące, nieoczekiwane i wyraźnie niepożądane zachowanie. Dlaczego to jest w porządku?

Barry
źródło
25
Czy nie jest C c{};inicjalizacja agregacji, więc żaden konstruktor nie jest wywoływany?
NathanOliver
5
Co powiedział @NathanOliver. Nie masz konstruktora dostarczonego przez użytkownika, więc Cjest to agregacja.
Kerrek SB
5
@KerrekSB W tym samym czasie było dla mnie dość zaskakujące, że użytkownik, który wyraźnie zadeklarował ctora, nie czyni go dostarczonym przez użytkownika.
Angew nie jest już dumny z SO
1
@Angew Dlatego wszyscy tu jesteśmy :)
Barry
2
@Angew Gdyby był to publiczny =defaultdoradca, wydawałoby się to bardziej rozsądne. Ale prywatny =defaultdoradca wydaje się ważną rzeczą, której nie należy ignorować. Co więcej, class C { C(); } inline C::C()=default;bycie całkiem innym jest nieco zaskakujące.
Yakk - Adam Nevraumont

Odpowiedzi:

61

Sztuczka jest w C ++ 14 8.4.2 / 5 [dcl.fct.def.default]:

... Funkcja jest dostarczana przez użytkownika, jeśli została zadeklarowana przez użytkownika i nie została jawnie ustawiona jako domyślna ani usunięta przy pierwszej deklaracji. ...

Co oznacza, że Cdomyślny konstruktor w rzeczywistości nie jest dostarczany przez użytkownika, ponieważ został jawnie ustawiony jako domyślny przy pierwszej deklaracji. W związku z tym Cnie ma konstruktorów dostarczonych przez użytkownika i dlatego jest agregatem według 8.5.1 / 1 [dcl.init.aggr]:

Agregat jest tablicą lub klasy (Rozdział 9) bez konstruktorów dostarczane przez użytkowników (12,1), ma prywatnych lub zabezpieczone nie-statycznych pól (punkt 11), brak zajęcia bazowe (punkt 10), oraz brak funkcji wirtualnej (10,3 ).

Angew nie jest już dumny z SO
źródło
13
W efekcie mały błąd standardowy: fakt, że domyślny ctor był prywatny, jest w efekcie ignorowany w tym kontekście.
Yakk - Adam Nevraumont
2
@Yakk, nie czuję się kwalifikowany, aby to oceniać. Sformułowanie o tym, że ctor nie jest dostarczany przez użytkownika, wygląda jednak bardzo celowo.
Angew nie jest już dumny z SO
1
@Yakk: Cóż, tak i nie. Gdyby klasa miała jakichkolwiek członków danych, miałbyś szansę uczynić je prywatnymi. Bez członków danych jest bardzo niewiele sytuacji, w których ta sytuacja miałaby poważny wpływ na kogokolwiek.
Kerrek SB
2
@KerrekSB Ma znaczenie, jeśli próbujesz użyć klasy jako pewnego rodzaju „tokenu dostępu”, kontrolującego np. Kto może wywołać funkcję na podstawie tego, kto może stworzyć obiekt klasy.
Angew nie jest już dumny z SO
5
@Yakk Jeszcze bardziej interesujące jest to, że C{}działa, nawet jeśli konstruktorem jest deleted.
Barry
56

Nie wywołujesz domyślnego konstruktora, używasz inicjalizacji agregacji na typie agregacji. Typy agregatów mogą mieć domyślny konstruktor, o ile jest on domyślny w miejscu, w którym został zadeklarowany po raz pierwszy:

Z [dcl.init.aggr] / 1 :

Agregat to tablica lub klasa (klauzula [klasa]) z

  • brak konstruktorów dostarczonych przez użytkownika ([class.ctor]) (w tym odziedziczonych ([namespace.udecl]) z klasy bazowej),
  • brak prywatnych lub chronionych niestatycznych składowych danych (klauzula [class.access]),
  • brak funkcji wirtualnych ([class.virtual]) i
  • brak wirtualnych, prywatnych lub chronionych klas bazowych ([class.mi]).

i z [dcl.fct.def.default] / 5

Funkcje z jawnie domyślnymi funkcjami i niejawnie zadeklarowane funkcje są zbiorczo nazywane funkcjami domyślnymi, a implementacja powinna zapewnić ich niejawne definicje ([class.ctor] [class.dtor], [class.copy]), co może oznaczać zdefiniowanie ich jako usuniętych . Funkcja jest dostarczana przez użytkownika, jeśli została zadeklarowana przez użytkownika i nie została jawnie ustawiona jako domyślna ani usunięta przy pierwszej deklaracji. Podana przez użytkownika funkcja z jawnie ustawioną domyślną wartością (tj. Jawnie ustawiona jako domyślna po jej pierwszej deklaracji) jest definiowana w miejscu, w którym została jawnie ustawiona jako domyślna; jeśli taka funkcja jest niejawnie zdefiniowana jako usunięta, program jest nieprawidłowo sformułowany.[Uwaga: Zadeklarowanie funkcji jako domyślnej po jej pierwszej deklaracji może zapewnić wydajne wykonanie i zwięzłą definicję, jednocześnie umożliwiając stabilny interfejs binarny dla rozwijającej się bazy kodu. - notatka końcowa]

Zatem nasze wymagania dotyczące kruszywa są następujące:

  • brak członków niepublicznych
  • brak funkcji wirtualnych
  • brak wirtualnych lub niepublicznych klas podstawowych
  • żadne konstruktory dostarczone przez użytkownika nie są dziedziczone ani inne, co zezwala tylko na konstruktory, które są:
    • niejawnie zadeklarowana lub
    • jawnie zadeklarowane i jednocześnie zdefiniowane jako niewykonane.

C spełnia wszystkie te wymagania.

Oczywiście możesz pozbyć się tego fałszywego domyślnego zachowania konstrukcji, po prostu dostarczając pustego domyślnego konstruktora lub definiując konstruktor jako domyślny po zadeklarowaniu go:

class C {
    C(){}
};
// --or--
class C {
    C();
};
inline C::C() = default;
jaggedSpire
źródło
2
Ta odpowiedź podoba mi się nieco lepiej niż ta autorstwa Angew, ale myślę, że przydałoby się podsumowanie na początku w co najwyżej dwóch zdaniach.
PJTraill
7

Angew's i jaggedSpire są doskonałe i odnoszą się do. I. I.

Jednak w , rzeczy się trochę zmieniają i przykład w PO nie będzie się już kompilować:

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

Jak wskazano w dwóch odpowiedziach, powodem, dla którego dwie ostatnie deklaracje działają, jest to, że Cjest to agregacja, a jest to inicjalizacja agregacji. Jednak w wyniku P1008 (przy użyciu motywującego przykładu nie różniącego się zbytnio od PO), definicja zagregowanych zmian w C ++ 20 do, z [dcl.init.aggr] / 1 :

Agregat to tablica lub klasa ([klasa]) z

  • brak konstruktorów zadeklarowanych przez użytkownika lub dziedziczonych ([class.ctor]),
  • brak prywatnych lub chronionych bezpośrednich niestatycznych składowych danych ([class.access]),
  • brak funkcji wirtualnych ([class.virtual]) i
  • brak wirtualnych, prywatnych lub chronionych klas bazowych ([class.mi]).

Podkreśl moje. Teraz wymaganiem jest brak konstruktorów zadeklarowanych przez użytkownika , podczas gdy kiedyś (jak obaj użytkownicy cytują w swoich odpowiedziach i można je przeglądać historycznie dla C ++ 11 , C ++ 14 i C ++ 17 ) nie było konstruktorów dostarczonych przez użytkownika . Domyślny konstruktor dla Cjest zadeklarowany przez użytkownika, ale nie jest dostarczany przez użytkownika, dlatego przestaje być agregacją w C ++ 20.


Oto kolejny przykład ilustrujący zagregowane zmiany:

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

Bnie był agregatem w C ++ 11 ani C ++ 14, ponieważ ma klasę bazową. W rezultacie B{}po prostu wywołuje domyślny konstruktor (zadeklarowany przez użytkownika, ale nie dostarczony przez użytkownika), który ma dostęp do Achronionego konstruktora domyślnego.

W C ++ 17, w wyniku P0017 , agregaty zostały rozszerzone o klasy bazowe. Bjest agregatem w C ++ 17, co oznacza, że B{}jest to inicjalizacja agregatu, która musi zainicjować wszystkie podobiekty - w tym Apodobiekt. Ale ponieważ Adomyślny konstruktor jest chroniony, nie mamy do niego dostępu, więc ta inicjalizacja jest źle sformułowana.

W C ++ 20, z powodu Bkonstruktora zadeklarowanego przez użytkownika, ponownie przestaje być agregatem, więc B{}powraca do wywoływania domyślnego konstruktora i jest to ponownie dobrze sformułowana inicjalizacja.

Barry
źródło