Nowa składnia „= default” w C ++ 11


136

Nie rozumiem, dlaczego miałbym to zrobić:

struct S { 
    int a; 
    S(int aa) : a(aa) {} 
    S() = default; 
};

Dlaczego po prostu nie powiedzieć:

S() {} // instead of S() = default;

po co wprowadzać do tego nową składnię?


30
Nitpick: defaultnie jest nowym słowem kluczowym, jest po prostu nowym użyciem już zarezerwowanego słowa kluczowego.


Mey be To pytanie może ci pomóc.
FreeNickname

7
Oprócz innych odpowiedzi twierdziłbym również, że „= default;” jest bardziej samodokumentująca.
Mark

Odpowiedzi:


136

Domyślny konstruktor domyślny jest specjalnie zdefiniowany jako taki sam, jak konstruktor domyślny zdefiniowany przez użytkownika bez listy inicjalizacyjnej i pustej instrukcji złożonej.

§12.1 / 6 [class.ctor] Domyślny konstruktor, który jest domyślny i nie jest zdefiniowany jako usunięty, jest niejawnie definiowany, gdy jest używany do tworzenia obiektu typu klasy odr lub gdy jest jawnie domyślny po pierwszej deklaracji. Domyślnie zdefiniowany konstruktor domyślny wykonuje zestaw inicjalizacji klasy, które byłyby wykonywane przez domyślny konstruktor napisany przez użytkownika dla tej klasy bez inicjatora ctor (12.6.2) i pustej instrukcji złożonej. […]

Jednak mimo że oba konstruktory zachowują się tak samo, podanie pustej implementacji ma wpływ na niektóre właściwości klasy. Podanie konstruktora zdefiniowanego przez użytkownika, mimo że nic nie robi, sprawia, że ​​typ nie jest agregatem, a także nie jest trywialny . Jeśli chcesz, aby twoja klasa była typem zagregowanym lub trywialnym (lub przez przechodniość, typ POD), musisz użyć = default.

§8.5.1 / 1 [dcl.init.aggr] Agregat to tablica lub klasa bez konstruktorów dostarczonych przez użytkownika, [i ...]

§12.1 / 5 [class.ctor] Domyślny konstruktor jest trywialny, jeśli nie jest dostarczony przez użytkownika i [...]

§9 / 6 [klasa] Trywialna klasa to klasa, która ma trywialny domyślny konstruktor i [...]

Aby zademonstrować:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() { };
};

int main() {
    static_assert(std::is_trivial<X>::value, "X should be trivial");
    static_assert(std::is_pod<X>::value, "X should be POD");
    
    static_assert(!std::is_trivial<Y>::value, "Y should not be trivial");
    static_assert(!std::is_pod<Y>::value, "Y should not be POD");
}

Ponadto jawne domyślne ustawienie konstruktora spowoduje to constexpr jeśli niejawny konstruktor byłby i zapewni mu taką samą specyfikację wyjątku, jaką miałby niejawny konstruktor. W przypadku, który podałeś, niejawny konstruktor nie byłby constexpr(ponieważ pozostawiłby niezainicjowany element członkowski danych), a także miałby pustą specyfikację wyjątku, więc nie ma różnicy. Ale tak, w ogólnym przypadku można ręcznie określić constexpri specyfikację wyjątku, aby dopasować niejawny konstruktor.

Używanie = defaultprzynosi pewną jednolitość, ponieważ może być również używane z konstruktorami kopiowania / przenoszenia i destruktorami. Na przykład pusty konstruktor kopiujący nie będzie działał tak samo, jak domyślny konstruktor kopiujący (który wykona kopię składową swoich elementów członkowskich). Użycie składni = default(lub = delete) jednolicie dla każdej z tych specjalnych funkcji składowych sprawia, że ​​kod jest łatwiejszy do odczytania, wyraźnie określając swój zamiar.


Prawie. 12.1 / 6: "Jeśli ten domyślny konstruktor napisany przez użytkownika spełniałby wymagania constexprkonstruktora (7.1.5), domyślnie zdefiniowany konstruktor domyślny jest constexpr."
Casey

W rzeczywistości 8.4.2 / 2 jest bardziej pouczające: „Jeśli funkcja jest jawnie domyślna w swojej pierwszej deklaracji, (a) jest domyślnie uznawana za constexprtaką, jeśli deklaracja domniemana byłaby, (b) zakłada się, że ma to samo specyfikacja wyjątku tak, jakby była niejawnie zadeklarowana (15.4), ... "Nie ma różnicy w tym konkretnym przypadku, ale ogólnie foo() = default;ma niewielką przewagę nad foo() {}.
Casey

2
Mówisz, że nie ma różnicy, a następnie wyjaśniasz różnice?

@hvd W tym przypadku nie ma różnicy, ponieważ niejawna deklaracja nie byłaby constexpr(ponieważ element członkowski danych pozostaje niezainicjowany), a jego specyfikacja wyjątku dopuszcza wszystkie wyjątki. Wyjaśnię to jaśniej.
Joseph Mansfield,

2
Dziękuję za wyjaśnienie. Wydaje się jednak, że nadal istnieje różnica, z constexprktórą (o której wspomniałeś, nie powinno robić różnicy): podaje struct S1 { int m; S1() {} S1(int m) : m(m) {} }; struct S2 { int m; S2() = default; S2(int m) : m(m) {} }; constexpr S1 s1 {}; constexpr S2 s2 {};tylko s1błąd, nie s2. W obu clang i g ++.

10

Mam przykład, który pokaże różnicę:

#include <iostream>

using namespace std;
class A 
{
public:
    int x;
    A(){}
};

class B 
{
public:
    int x;
    B()=default;
};


int main() 
{ 
    int x = 5;
    new(&x)A(); // Call for empty constructor, which does nothing
    cout << x << endl;
    new(&x)B; // Call for default constructor
    cout << x << endl;
    new(&x)B(); // Call for default constructor + Value initialization
    cout << x << endl;
    return 0; 
} 

Wynik:

5
5
0

Jak widać, wywołanie pustego konstruktora A () nie inicjalizuje członków, podczas gdy B () to robi.


7
czy mógłbyś wyjaśnić tę składnię -> new (& x) A ();
Vencat

5
Tworzymy nowy obiekt w pamięci zaczynając od adresu zmiennej x (zamiast nowej alokacji pamięci). Ta składnia służy do tworzenia obiektu we wstępnie przydzielonej pamięci. Tak jak w naszym przypadku rozmiar B = rozmiar int, tak new (& x) A () utworzy nowy obiekt w miejsce zmiennej x.
Slavenskij

Dziękuję za wyjaśnienie.
Vencat,

1
Otrzymuję inne wyniki z gcc 8.3: ideone.com/XouXux
Adam.Er8

Nawet z C ++ 14 otrzymuję różne wyniki: ideone.com/CQphuT
Mayank Bhushan,

9

n2210 podaje kilka powodów:

Zarządzanie domyślnymi ustawieniami ma kilka problemów:

  • Definicje konstruktorów są powiązane; zadeklarowanie dowolnego konstruktora pomija domyślny konstruktor.
  • Wartość domyślna destruktora jest nieodpowiednia dla klas polimorficznych i wymaga jawnej definicji.
  • Po zniesieniu wartości domyślnej nie ma możliwości jej wskrzeszenia.
  • Implementacje domyślne są często bardziej wydajne niż implementacje określone ręcznie.
  • Implementacje inne niż domyślne są nietrywialne, co wpływa na semantykę typów, np. Sprawia, że ​​typ jest inny niż POD.
  • Nie ma sposobu, aby zabronić specjalnej funkcji składowej lub operatora globalnego bez deklarowania (nietrywialnego) substytutu.

type::type() = default;
type::type() { x = 3; }

W niektórych przypadkach treść klasy może ulec zmianie bez konieczności zmiany definicji funkcji składowej, ponieważ wartość domyślna zmienia się wraz z deklaracją dodatkowych członków.

Zobacz, jak zasada trzech staje się regułą pięciu w C ++ 11? :

Zauważ, że konstruktor przenoszenia i operator przypisania przeniesienia nie zostaną wygenerowane dla klasy, która jawnie deklaruje żadną z innych specjalnych funkcji składowych, ten konstruktor kopiujący i operator przypisania kopiującego nie zostaną wygenerowane dla klasy, która jawnie deklaruje konstruktor przenoszenia lub przenoszenie operator przypisania i że klasa z jawnie zadeklarowanym destruktorem i niejawnie zdefiniowanym konstruktorem kopiującym lub niejawnie zdefiniowanym operatorem przypisania kopii jest uważana za przestarzałą


1
Są powody konieczności = defaultw ogóle, a nie powody = defaultna konstruktora vs. robi { }.
Joseph Mansfield

@JosephMansfield prawda, ale ponieważ {}był już cechą języka przed wprowadzeniem =defaulttych powodów nie niejawnie polegać na różnicy (np „nie ma możliwości wskrzeszenia [a tłumione domyślnie]” oznacza, że {}to nie równoważna Domyślnie ).
Kyle Strand

7

W niektórych przypadkach jest to kwestia semantyki. Nie jest to zbyt oczywiste w przypadku domyślnych konstruktorów, ale staje się oczywiste w przypadku innych funkcji składowych generowanych przez kompilator.

W przypadku konstruktora domyślnego byłoby możliwe, aby każdy domyślny konstruktor z pustym ciałem był uważany za kandydata do bycia konstruktorem trywialnym, tak samo jak using =default. W końcu stare puste domyślne konstruktory były legalne w C ++ .

struct S { 
  int a; 
  S() {} // legal C++ 
};

To, czy kompilator rozumie, że ten konstruktor jest trywialny, w większości przypadków nie ma znaczenia poza optymalizacjami (ręcznymi lub kompilatorowymi).

Jednak ta próba traktowania pustych treści funkcji jako „domyślnych” nie działa całkowicie na inne typy funkcji składowych. Rozważmy konstruktor kopiujący:

struct S { 
  int a; 
  S() {}
  S(const S&) {} // legal, but semantically wrong
};

W powyższym przypadku konstruktor kopiujący napisany z pustą treścią jest teraz nieprawidłowy . To już nie jest kopiowanie niczego. Jest to zupełnie inny zestaw semantyki niż domyślna semantyka konstruktora kopiującego. Pożądane zachowanie wymaga napisania kodu:

struct S { 
  int a; 
  S() {}
  S(const S& src) : a(src.a) {} // fixed
};

Jednak nawet w tym prostym przypadku staje się znacznie większym obciążeniem dla kompilatora, aby sprawdzić, czy konstruktor kopiujący jest identyczny z tym, który sam wygenerowałby, lub aby zobaczyć, że konstruktor kopiujący jest trywialny (odpowiednikmemcpy , w zasadzie ). Kompilator musiałby sprawdzić każde wyrażenie inicjalizujące element członkowski i upewnić się, że jest identyczne z wyrażeniem, aby uzyskać dostęp do odpowiedniego elementu członkowskiego źródła i nic więcej, upewnić się, że żaden element członkowski nie został pozostawiony z nietrywialną domyślną konstrukcją itp. Jest to wsteczna droga procesu kompilator użyłby do sprawdzenia, czy jego własne wygenerowane wersje tej funkcji są trywialne.

Rozważmy zatem operator przypisania kopiowania, który może być jeszcze bardziej skomplikowany, zwłaszcza w nietrywialnym przypadku. To mnóstwo gotowej płyty, której nie chcesz pisać dla wielu klas, ale i tak jesteś do tego zmuszony w C ++ 03:

struct T { 
  std::shared_ptr<int> b; 
  T(); // the usual definitions
  T(const T&);
  T& operator=(const T& src) {
    if (this != &src) // not actually needed for this simple example
      b = src.b; // non-trivial operation
    return *this;
};

To prosty przypadek, ale jest to już więcej kodu, niż kiedykolwiek chciałbyś być zmuszony do pisania dla tak prostego typu T(zwłaszcza gdy wrzucimy operacje przenoszenia do miksu). Nie możemy polegać na pustym treści oznaczającym „wypełnij wartości domyślne”, ponieważ puste ciało jest już w pełni poprawne i ma jasne znaczenie. W rzeczywistości, gdyby puste ciało zostało użyte do oznaczenia „wypełnij wartości domyślne”, nie byłoby sposobu na jawne utworzenie konstruktora kopiującego bez operacji lub podobnego.

To znowu kwestia spójności. Puste ciało oznacza „nic nie rób”, ale w przypadku takich rzeczy, jak konstruktory kopiujące, naprawdę nie chcesz „nic nie robić”, a raczej „robić wszystko, co normalnie robisz, jeśli nie jest to wstrzymane”. Stąd =default. Jest to konieczne do przezwyciężenia pominiętych funkcji składowych generowanych przez kompilator, takich jak konstruktory kopiowania / przenoszenia i operatory przypisania. Wtedy jest to po prostu „oczywiste”, aby działał również dla domyślnego konstruktora.

Byłoby fajnie, gdyby domyślny konstruktor z pustymi treściami i trywialnymi konstruktorami składowymi / bazowymi również był uważany za trywialne, tak jak byłyby =defaultw niektórych przypadkach, gdyby tylko uczynić starszy kod bardziej optymalnym w niektórych przypadkach, ale większość kodu niskiego poziomu opiera się na trywialnych domyślne konstruktory do optymalizacji również opierają się na trywialnych konstruktorach kopiujących. Jeśli będziesz musiał iść i "naprawić" wszystkie swoje stare konstruktory kopiowania, naprawianie wszystkich starych domyślnych konstruktorów nie jest zbyt trudne. O wiele jaśniejsze i bardziej oczywiste jest również użycie wyraźnego =defaultwyrażenia, aby wskazać swoje intencje.

Jest jeszcze kilka innych rzeczy, które będą robione przez funkcje składowe generowane przez kompilator, a które będziesz musiał jawnie wprowadzić zmiany w obsłudze. Wspieranie constexprdla domyślnych konstruktorów jest jednym z przykładów. Jest to po prostu łatwiejsze w użyciu psychicznie =defaultniż konieczność oznaczania funkcji wszystkimi innymi specjalnymi słowami kluczowymi i takimi, które są implikowane =defaulti było jednym z tematów C ++ 11: uczynić język łatwiejszym. Wciąż ma wiele brodawek i kompromisów z kompatybilnością wsteczną, ale jasne jest, że jest to duży krok naprzód w stosunku do C ++ 03, jeśli chodzi o łatwość użycia.


Miałem problem w miejscu, w którym się spodziewałem = default, a=0;a nie było! Musiałem to rzucić na korzyść : a(0). Nadal nie wiem, jak przydatne = defaultjest to, czy chodzi o wydajność? czy gdzieś się zepsuje, jeśli po prostu nie użyję = default? Próbowałem przeczytać wszystkie odpowiedzi tutaj kup Jestem nowy w niektórych rzeczach C ++ i mam dużo problemów ze zrozumieniem tego.
Aquarius Power

@AquariusPower: nie chodzi „tylko” o wydajność, ale w niektórych przypadkach jest to wymagane w przypadku wyjątków i innej semantyki. Mianowicie, domyślny operator może być trywialny, ale taki, który nie ma domyślnej wartości, nigdy nie może być trywialny, a niektóre kody wykorzystują techniki metaprogramowania, aby zmienić zachowanie lub nawet zabronić typów z nietrywialnymi operacjami. Twój a=0przykład wynika z zachowania trywialnych typów, które są osobnym (choć pokrewnym) tematem.
Sean Middleditch,

czy to znaczy, że można mieć = defaulti nadal abędzie przyznawać =0? w pewnym sensie? czy myślisz, że mógłbym stworzyć nowe pytanie typu "jak mieć konstruktora = defaulti przyznać, że pola będą poprawnie zainicjalizowane?", btw miałem problem w a structa nie a class, a aplikacja działa poprawnie nawet nie używam = default, mogę dodaj minimalną strukturę do tego pytania, jeśli jest dobra :)
Aquarius Power,

1
@AquariusPower: możesz użyć statycznych inicjatorów składowych danych. Napisz swoją strukturę w ten sposób: struct { int a = 0; };Jeśli następnie zdecydujesz, że potrzebujesz konstruktora, możesz go ustawić jako domyślny, ale pamiętaj, że typ nie będzie trywialny (co jest w porządku).
Sean Middleditch

2

Z powodu wycofania std::is_podi alternatywy std::is_trivial && std::is_standard_layoutfragment odpowiedzi @JosephMansfield wygląda następująco:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() {}
};

int main() {
    static_assert(std::is_trivial_v<X>, "X should be trivial");
    static_assert(std::is_standard_layout_v<X>, "X should be standard layout");

    static_assert(!std::is_trivial_v<Y>, "Y should not be trivial");
    static_assert(std::is_standard_layout_v<Y>, "Y should be standard layout");
}

Zwróć uwagę, że Ynadal ma standardowy układ.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.