Dlaczego C ++ wymaga domyślnego konstruktora dostarczonego przez użytkownika do domyślnego konstruowania obiektu stałego?

99

Standard C ++ (sekcja 8.5) mówi:

Jeśli program wywołuje domyślną inicjalizację obiektu typu T z kwalifikowaną wartością stałą, T będzie typem klasy z domyślnym konstruktorem dostarczonym przez użytkownika.

Czemu? Nie przychodzi mi do głowy żaden powód, dla którego w tym przypadku wymagany jest konstruktor dostarczony przez użytkownika.

struct B{
  B():x(42){}
  int doSomeStuff() const{return x;}
  int x;
};

struct A{
  A(){}//other than "because the standard says so", why is this line required?

  B b;//not required for this example, just to illustrate
      //how this situation isn't totally useless
};

int main(){
  const A a;
}
Karu
źródło
2
Linia nie wydaje się być wymagana w twoim przykładzie (patrz ideone.com/qqiXR ), ponieważ zadeklarowałeś, ale nie zdefiniowałeś / zainicjowałeś a, ale gcc-4.3.4 akceptuje to nawet wtedy, gdy to zrobisz (patrz ideone.com/uHvFS )
Ray Toal
Powyższy przykład deklaruje i definiuje a . Comeau generuje błąd „zmienna stała” a wymaga inicjatora - klasa „A” nie ma jawnie zadeklarowanego domyślnego konstruktora ”, jeśli linia jest wykomentowana.
Karu,
4
Naprawiono to w C ++ 11, możesz pisać const A a{}:)
Howard Lovatt

Odpowiedzi:

10

Uznano to za wadę (w stosunku do wszystkich wersji standardu) i rozwiązała ją Core Working Group (CWG) Defect 253 . Nowe brzmienie standardu to http://eel.is/c++draft/dcl.init#7

Typ klasy T jest konstruowalny jako const-default, jeśli domyślna inicjalizacja T spowodowałaby wywołanie konstruktora T dostarczonego przez użytkownika (nie jest dziedziczony z klasy bazowej) lub jeśli

  • każdy bezpośredni, niezmienny, niestatyczny element członkowski danych M z T ma domyślny inicjator elementu członkowskiego lub, jeśli M jest typu klasy X (lub jego tablicy), X jest konstruowalny jako const-default,
  • jeśli T jest unią z co najmniej jednym niestatycznym składnikiem danych, dokładnie jeden element członkowski wariantu ma domyślny inicjator elementu członkowskiego,
  • jeśli T nie jest unią, dla każdego anonimowego członka unii z co najmniej jednym niestatycznym składnikiem danych (jeśli istnieje), dokładnie jeden niestatyczny element członkowski danych ma domyślny inicjator elementu członkowskiego i
  • każda potencjalnie skonstruowana klasa bazowa T jest konstruowana jako stała domyślna.

Jeśli program wywołuje domyślną inicjalizację obiektu typu T z kwalifikowaną wartością stałą, T będzie typem klasy lub tablicą z możliwością konstruowania typu const-default.

To sformułowanie zasadniczo oznacza, że ​​działa oczywisty kod. Jeśli zainicjujesz wszystkie swoje bazy i członków, możesz powiedzieć A const a;niezależnie od tego, jak lub czy przeliterujesz jakiekolwiek konstruktory.

struct A {
};
A const a;

gcc akceptuje to od 4.6.4. clang zaakceptował to od wersji 3.9.0. Visual Studio również to akceptuje (przynajmniej w 2017 roku, nie wiem, czy wcześniej).

David Stone
źródło
3
Ale to nadal zabrania struct A { int n; A() = default; }; const A a;, pozwalając, struct B { int n; B() {} }; const B b;ponieważ nowe sformułowanie nadal mówi „dostarczone przez użytkownika”, a nie „zadeklarowane przez użytkownika”, a ja drapię się po głowie, dlaczego komitet zdecydował się wykluczyć z tego DR konstruktory z jawnie domyślnymi ustawieniami domyślnymi, zmuszając nas do nasze klasy są nietrywialne, jeśli chcemy mieć obiekty const z niezainicjowanymi składowymi.
Oktalist
1
Ciekawe, ale wciąż jest przypadek, z którym się spotkałem. Z MyPODbycia POD struct, static MyPOD x;- opierając się na zero inicjalizacji (jest to słuszna?), Aby ustawić zmienną (y) Użytkownik odpowiednio - kompiluje, ale static const MyPOD x;tego nie robi. Czy jest szansa, że zostanie to naprawione?
Joshua Green
66

Powodem jest to, że jeśli klasa nie ma konstruktora zdefiniowanego przez użytkownika, może to być POD, a klasa POD nie jest domyślnie inicjowana. Więc jeśli zadeklarujesz stały obiekt POD, który jest niezainicjalizowany, po co z tego? Myślę więc, że standard wymusza tę zasadę, aby obiekt faktycznie był użyteczny.

struct POD
{
  int i;
};

POD p1; //uninitialized - but don't worry we can assign some value later on!
p1.i = 10; //assign some value later on!

POD p2 = POD(); //initialized

const POD p3 = POD(); //initialized 

const POD p4; //uninitialized  - error - as we cannot change it later on!

Ale jeśli ustawisz klasę jako inną niż POD:

struct nonPOD_A
{
    nonPOD_A() {} //this makes non-POD
};

nonPOD_A a1; //initialized 
const nonPOD_A a2; //initialized 

Zwróć uwagę na różnicę między POD i bez POD.

Konstruktor zdefiniowany przez użytkownika jest jednym ze sposobów uczynienia klasy inną niż POD. Możesz to zrobić na kilka sposobów.

struct nonPOD_B
{
    virtual void f() {} //virtual function make it non-POD
};

nonPOD_B b1; //initialized 
const nonPOD_B b2; //initialized 

Zauważ, że nonPOD_B nie ma zdefiniowanego konstruktora zdefiniowanego przez użytkownika. Skompiluj to. Skompiluje:

I skomentuj funkcję wirtualną, a następnie zgodnie z oczekiwaniami podaje błąd:


Myślę, że źle zrozumiałeś ten fragment. Najpierw mówi to (§ 8.5 / 9):

Jeśli dla obiektu nie określono inicjatora, a obiekt jest (prawdopodobnie kwalifikowany przez cv) typem innym niż POD (lub jego tablicą), obiekt powinien zostać zainicjowany domyślnie; […]

Mówi o typie bez klasy POD, który może kwalifikować się jako CV . Oznacza to, że obiekt inny niż POD powinien być inicjalizowany domyślnie, jeśli nie określono inicjatora. A co to jest inicjalizacja domyślna ? W przypadku braku POD, specyfikacja mówi (§8.5 / 5),

Domyślne zainicjowanie obiektu typu T oznacza:
- jeśli T jest typem klasy innym niż POD (klauzula 9), wywoływany jest domyślny konstruktor dla T (a inicjalizacja jest źle sformułowana, jeśli T nie ma dostępnego domyślnego konstruktora);

Po prostu mówi o domyślnym konstruktorze T, niezależnie od tego, czy jego zdefiniowany przez użytkownika, czy wygenerowany przez kompilator jest nieistotny.

Jeśli masz jasność co do tego, zrozum, co mówi dalej specyfikacja ((§8.5 / 9),

[...]; jeśli obiekt jest typu const-qualified, typ klasy bazowej powinien mieć domyślnego konstruktora zadeklarowanego przez użytkownika.

Tak więc ten tekst sugeruje, że program będzie źle sformułowany, jeśli obiekt jest typu POD o stałej kwalifikacji i nie określono inicjatora (ponieważ POD nie są domyślnie zainicjowane):

POD p1; //uninitialized - can be useful - hence allowed
const POD p2; //uninitialized - never useful  - hence not allowed - error

Nawiasem mówiąc, kompiluje się dobrze , ponieważ nie jest to POD i może być inicjalizowany domyślnie .

Nawaz
źródło
1
Uważam, że ostatnim przykładem jest błąd kompilacji - nonPOD_Bnie ma domyślnego konstruktora podanego przez użytkownika, więc wiersz const nonPOD_B b2nie jest dozwolony.
Karu,
1
Innym sposobem uczynienia klasy inną niż POD jest podanie jej członka danych, który nie jest POD (np. Moja struktura Bw pytaniu). Jednak w tym przypadku nadal wymagany jest domyślny konstruktor dostarczony przez użytkownika.
Karu,
„Jeśli program wywołuje domyślną inicjalizację obiektu typu T z kwalifikacją stałą, T będzie typem klasy z domyślnym konstruktorem dostarczonym przez użytkownika”.
Karu,
@Karu: Czytałem to. Wygląda na to, że w specyfikacji są inne fragmenty, które pozwalają constna zainicjowanie obiektu innego niż POD przez wywołanie domyślnego konstruktora wygenerowanego przez kompilator.
Nawaz,
2
Twoje linki ideone wydają się być zepsute i byłoby wspaniale, gdyby ta odpowiedź mogła zostać zaktualizowana do C ++ 11/14, ponieważ §8.5 w ogóle nie wspomina o POD.
Oktalist
12

Czysta spekulacja z mojej strony, ale weź pod uwagę, że inne typy również mają podobne ograniczenie:

int main()
{
    const int i; // invalid
}

Tak więc ta reguła jest nie tylko spójna, ale także (rekurencyjnie) zapobiega zjednostkowanym const(pod) obiektom:

struct X {
    int j;
};
struct A {
    int i;
    X x;
}

int main()
{
    const A a; // a.i and a.x.j in unitialized states!
}

Jeśli chodzi o drugą stronę pytania (zezwalając na typy z domyślnym konstruktorem), myślę, że idea jest taka, że ​​typ z domyślnym konstruktorem dostarczonym przez użytkownika powinien zawsze znajdować się w jakimś sensownym stanie po skonstruowaniu. Zwróć uwagę, że reguły, jakie są, pozwalają na:

struct A {
    explicit
    A(int i): initialized(true), i(i) {} // valued constructor

    A(): initialized(false) {}

    bool initialized;
    int i;
};

const A a; // class invariant set up for the object
           // yet we didn't pay the cost of initializing a.i

Wtedy być może moglibyśmy sformułować regułę w rodzaju „przynajmniej jeden element członkowski musi być rozsądnie zainicjowany w domyślnym konstruktorze dostarczonym przez użytkownika”, ale to zbyt dużo czasu spędzonego na próbach ochrony przed Murphym. C ++ ma tendencję do ufania programiście w pewnych punktach.

Luc Danton
źródło
Ale po dodaniu A(){}błąd zniknie, więc nic nie zapobiega. Reguła nie działa rekurencyjnie - X(){}w tym przykładzie nigdy nie jest potrzebna.
Karu,
2
Cóż, przynajmniej zmuszając programistę do dodania konstruktora, jest zmuszony do zastanowienia się nad problemem i może wymyślił nietrywialny
arne
@Karu Odpowiedziałem tylko na połowę pytania - naprawiłem to :)
Luc Danton
4
@arne: Jedynym problemem jest to, że to zły programista. Osoba próbująca utworzyć instancję klasy może poświęcić całej sprawie myśli, której chce, ale może nie być w stanie zmodyfikować klasy. Autor klasy pomyślał o członkach, zobaczył, że wszystkie zostały rozsądnie zainicjowane przez domyślny konstruktor, więc nigdy nie dodawał żadnego.
Karu
3
Z tej części standardu zaczerpnąłem: „zawsze deklaruj domyślny konstruktor dla typów innych niż POD, na wypadek, gdyby ktoś chciał kiedyś utworzyć instancję const”. Wydaje się, że to trochę przesada.
Karu,
3

Oglądałem przemówienie Timura Doumlera na Meeting C ++ 2018 i w końcu zdałem sobie sprawę, dlaczego standard wymaga tutaj konstruktora dostarczonego przez użytkownika, a nie tylko zadeklarowanego przez użytkownika. Ma to związek z regułami inicjalizacji wartości.

Rozważ dwie klasy: Ama konstruktora zadeklarowanegoB przez użytkownika , ma konstruktora dostarczonego przez użytkownika :

struct A {
    int x;
    A() = default;
};
struct B {
    int x;
    B() {}
};

Na pierwszy rzut oka możesz pomyśleć, że ci dwaj konstruktorzy będą się zachowywać tak samo. Ale zobacz, jak inicjalizacja wartości zachowuje się inaczej, podczas gdy tylko domyślna inicjalizacja zachowuje się tak samo:

  • A a;jest domyślną inicjalizacją: element członkowski nie int xzostał zainicjowany .
  • B b;jest domyślną inicjalizacją: element członkowski nie int xzostał zainicjowany .
  • A a{};jest inicjalizacją wartości: element członkowski int xjest inicjalizowany przez zero .
  • B b{};jest inicjalizacją wartości: element członkowski nie int xzostał zainicjowany .

Teraz zobacz, co się stanie, gdy dodamy const:

  • const A a;jest domyślną inicjalizacją: jest źle sformułowana z powodu reguły przytoczonej w pytaniu.
  • const B b;jest domyślną inicjalizacją: element członkowski nie int xzostał zainicjowany .
  • const A a{};jest inicjalizacją wartości: element członkowski int xjest inicjalizowany przez zero .
  • const B b{};jest inicjalizacją wartości: element członkowski nie int xzostał zainicjowany .

Niezainicjalizowany constskalar (np. Element int xczłonkowski) byłby bezużyteczny: pisanie do niego jest źle sformułowane (ponieważ jest const), a czytanie z niego to UB (ponieważ posiada nieokreśloną wartość). Tak więc zasada ta zapobiega tworzeniu takiego, zmuszając cię do albo dodać initialiser lub opt-in do niebezpiecznych zachowań dodając konstruktora użytkownika warunkiem.

Myślę, że byłoby miło mieć taki atrybut, [[uninitialized]]który mówi kompilatorowi, kiedy celowo nie inicjalizujesz obiektu. Wtedy nie bylibyśmy zmuszeni uczynić naszej klasy niebędącą trywialnie domyślną konstrukcją, aby obejść ten narożny przypadek. Ten atrybut został faktycznie zaproponowany , ale tak jak wszystkie inne standardowe atrybuty, nie narzuca on żadnego normatywnego zachowania, będąc jedynie wskazówką dla kompilatora.

Oktalist
źródło
1

Gratulacje, wymyśliłeś przypadek, w którym nie musi istnieć żaden konstruktor zdefiniowany przez użytkownika dla constdeklaracji bez inicjatora, który miałby sens.

Czy możesz teraz wymyślić rozsądne przeredagowanie zasady, która obejmuje twoją sprawę, ale nadal sprawia, że ​​sprawy, które powinny być nielegalne, są nielegalne? Czy jest mniej niż 5 lub 6 akapitów? Czy jest łatwe i oczywiste, jak należy je zastosować w każdej sytuacji?

Uważam, że wymyślenie reguły, która pozwoli, aby deklaracja, którą stworzyłeś, miała sens, jest naprawdę trudna, a upewnienie się, że reguła może być stosowana w sposób, który ma sens dla ludzi podczas czytania kodu, jest jeszcze trudniejsze. Wolałbym nieco restrykcyjną regułę, która w większości przypadków była właściwa, od bardzo zniuansowanej i złożonej reguły, która była trudna do zrozumienia i zastosowania.

Pytanie brzmi, czy istnieje nieodparty powód, dla którego reguła powinna być bardziej złożona? Czy istnieje kod, który w innym przypadku byłby bardzo trudny do napisania lub zrozumienia, a który można napisać o wiele prościej, jeśli reguła jest bardziej złożona?

Wszelaki
źródło
1
Oto moje sugerowane sformułowanie: „Jeśli program wymaga domyślnej inicjalizacji obiektu typu T z kwalifikacją stałą, T powinno być klasą inną niż POD.”. To uczyniłoby const POD x;nielegalnym tak samo, jak const int x;jest nielegalny (co ma sens, ponieważ jest bezużyteczny dla POD), ale stałby się const NonPOD x;legalny (co ma sens, ponieważ może mieć podobiekty zawierające przydatne konstruktory / destruktory lub sam przydatny konstruktor / destruktor) .
Karu
@Karu - To sformułowanie może działać. Jestem przyzwyczajony do standardów RFC i uważam, że „T powinno być” powinno brzmieć „T musi być”. Ale tak, to może zadziałać.
Omnifarious
@Karu - A co ze struct NonPod {int i; virtual void f () {}}? Nie ma sensu uczynić const NonPod x; prawny.
gruzovator
1
@gruzovator Czy bardziej sensowne byłoby posiadanie pustego domyślnego konstruktora zadeklarowanego przez użytkownika? Moja sugestia jest tylko próbą usunięcia bezsensownego wymogu normy; z nią lub bez, wciąż istnieje nieskończenie wiele sposobów pisania kodu, który nie ma sensu.
Karu,
1
@Karu Zgadzam się z tobą. Z powodu tej reguły w standardzie istnieje wiele klas, które muszą mieć zdefiniowany przez użytkownika pusty konstruktor. Lubię zachowanie GCC. Pozwala np. struct NonPod { std::string s; }; const NonPod x;I wyświetla błąd, gdy NonPod tostruct NonPod { int i; std::string s; }; const NonPod x;
gruzovator