Czym różni się „= default” od „{}” dla domyślnego konstruktora i destruktora?

169

Pierwotnie opublikowałem to jako pytanie tylko o destruktory, ale teraz dodaję do rozważenia domyślnego konstruktora. Oto oryginalne pytanie:

Jeśli chcę dać mojej klasie destruktor, który jest wirtualny, ale poza tym jest taki sam, jak wygenerowałby kompilator, mogę użyć =default:

class Widget {
public:
   virtual ~Widget() = default;
};

Wygląda jednak na to, że ten sam efekt można uzyskać, mniej wpisując, używając pustej definicji:

class Widget {
public:
   virtual ~Widget() {}
};

Czy istnieje sposób, w jaki te dwie definicje zachowują się inaczej?

Na podstawie odpowiedzi przesłanych na to pytanie sytuacja domyślnego konstruktora wydaje się podobna. Biorąc pod uwagę, że nie ma prawie żadnej różnicy w znaczeniu między znakami „ =default” i „ {}” dla destruktorów, czy podobnie nie ma prawie żadnej różnicy w znaczeniu między tymi opcjami dla domyślnych konstruktorów? To znaczy, zakładając, że chcę utworzyć typ, w którym obiekty tego typu będą zarówno tworzone, jak i niszczone, dlaczego miałbym chcieć powiedzieć

Widget() = default;

zamiast

Widget() {}

?

Przepraszam, jeśli rozszerzenie tego pytania po jego oryginalnym opublikowaniu narusza niektóre zasady SO. Opublikowanie prawie identycznego pytania dla domyślnych konstruktorów wydało mi się mniej pożądaną opcją.

KnowItAllWannabe
źródło
1
Nie żebym wiedział, ale = defaultjest imo bardziej dosłowne i jest zgodne z obsługą go przez konstruktorów.
chris
11
Nie wiem na pewno, ale myślę, że to pierwsze odpowiada definicji „trywialnego destruktora”, podczas gdy drugie nie. To samo std::has_trivial_destructor<Widget>::valuedotyczy truepierwszego, ale falsedrugiego. Jakie są z tego konsekwencje, też nie wiem. :)
GManNickG
10
Wirtualny destruktor nigdy nie jest trywialny.
Luc Danton
@LucDanton: Przypuszczam, że otwarcie oczu i spojrzenie na kod też by zadziałało! Dzięki za poprawienie.
GManNickG
Związane z: stackoverflow.com/questions/20828907/…
Gabriel Staples

Odpowiedzi:

103

To zupełnie inne pytanie, kiedy pytamy o konstruktory niż destruktory.

Jeśli twoim destruktorem jest virtual, różnica jest znikoma, jak zauważył Howard . Jeśli jednak twój destruktor nie był wirtualny , to zupełnie inna historia. To samo dotyczy konstruktorów.

Używanie = defaultskładni dla specjalnych funkcji składowych (domyślny konstruktor, konstruktory kopiowania / przenoszenia / przypisanie, destruktory itp.) Oznacza coś zupełnie innego niż zwykłe robienie {}. W tym drugim przypadku funkcja staje się „dostarczana przez użytkownika”. A to wszystko zmienia.

To trywialna klasa według definicji C ++ 11:

struct Trivial
{
  int foo;
};

Jeśli spróbujesz utworzyć domyślny konstruktor, kompilator automatycznie wygeneruje domyślny konstruktor. To samo dotyczy kopiowania / przenoszenia i niszczenia. Ponieważ użytkownik nie dostarczył żadnej z tych funkcji składowych, specyfikacja C ++ 11 uważa tę klasę za „trywialną”. Dlatego jest to legalne, na przykład zapamiętywanie ich zawartości w celu ich zainicjowania i tak dalej.

To:

struct NotTrivial
{
  int foo;

  NotTrivial() {}
};

Jak sama nazwa wskazuje, nie jest to już trywialne. Ma domyślny konstruktor dostarczony przez użytkownika. Nie ma znaczenia, czy jest pusty; jeśli chodzi o reguły C ++ 11, nie może to być trywialny typ.

To:

struct Trivial2
{
  int foo;

  Trivial2() = default;
};

Jak sama nazwa wskazuje, jest to trywialny typ. Czemu? Ponieważ powiedziałeś kompilatorowi, aby automatycznie wygenerował domyślny konstruktor. Dlatego konstruktor nie jest „udostępniany przez użytkownika”. Dlatego typ liczy się jako trywialny, ponieważ nie ma dostarczonego przez użytkownika domyślnego konstruktora.

= defaultSkładnia jest głównie tam robi takie rzeczy jak kopiowanie konstruktorów / cesja podczas dodawania funkcji składowych, które uniemożliwiają tworzenie takich funkcji. Ale wyzwala również specjalne zachowanie kompilatora, więc jest przydatne również w domyślnych konstruktorach / destruktorach.

Nicol Bolas
źródło
2
Dlatego kluczową kwestią wydaje się być to, czy wynikowa klasa jest trywialna, a podstawą tego problemu jest różnica między zadeklarowaną przez użytkownika funkcją specjalną (co ma miejsce w przypadku =defaultfunkcji) a funkcjami dostarczanymi przez użytkownika (co ma miejsce w przypadku {}). Zarówno funkcje zadeklarowane przez użytkownika, jak i funkcje dostarczone przez użytkownika mogą zapobiegać generowaniu innych specjalnych funkcji składowych (np. Destruktor zadeklarowany przez użytkownika zapobiega generowaniu operacji przenoszenia), ale tylko funkcja specjalna dostarczona przez użytkownika sprawia, że ​​klasa jest nietrywialna. Dobrze?
KnowItAllWannabe
@KnowItAllWannabe: To jest ogólna idea, tak.
Nicol Bolas
Wybieram to jako akceptowaną odpowiedź, tylko dlatego, że obejmuje zarówno konstruktory, jak i (w odniesieniu do odpowiedzi Howarda) destruktory.
KnowItAllWannabe
Wydaje się, że jest tu brakującym słowem "jeśli chodzi o zasady C ++ 11, masz prawa trywialnego typu" Naprawiłbym to, ale nie jestem w 100% pewien, co było zamierzone.
jcoder
2
= defaultwydaje się być przydatny do wymuszania na kompilatorze wygenerowania domyślnego konstruktora pomimo obecności innych konstruktorów; domyślny konstruktor nie jest niejawnie zadeklarowany, jeśli podano inne konstruktory zadeklarowane przez użytkownika.
bgfvdu3w
42

Oba są nietrywialne.

Obie mają tę samą specyfikację noexcept w zależności od specyfikacji noexcept podstaw i elementów członkowskich.

Jedyną różnicą, którą do tej pory wykrywam, jest to, że jeśli Widgetzawiera bazę lub element członkowski z niedostępnym lub usuniętym destruktorem:

struct A
{
private:
    ~A();
};

class Widget {
    A a_;
public:
#if 1
   virtual ~Widget() = default;
#else
   virtual ~Widget() {}
#endif
};

Wtedy =defaultrozwiązanie zostanie skompilowane, ale Widgetnie będzie typu zniszczalnego. To znaczy, jeśli spróbujesz zniszczyć plik Widget, pojawi się błąd kompilacji. Ale jeśli tego nie zrobisz, masz działający program.

Otoh, jeśli dostarczysz destruktor dostarczony przez użytkownika , to rzeczy się nie skompilują, niezależnie od tego, czy zniszczysz Widget:

test.cpp:8:7: error: field of type 'A' has private destructor
    A a_;
      ^
test.cpp:4:5: note: declared private here
    ~A();
    ^
1 error generated.
Howard Hinnant
źródło
9
Interesujące: innymi słowy, =default;kompilator nie wygeneruje destruktora, chyba że zostanie użyty, a zatem nie spowoduje błędu. Wydaje mi się to dziwne, nawet jeśli niekoniecznie jest to błąd. Nie mogę sobie wyobrazić, że takie zachowanie jest wymagane w standardzie.
Nik Bougalis
„Wtedy rozwiązanie = default zostanie skompilowane” Nie, nie będzie. Tylko przetestowane vs.
nano
Jaki był komunikat o błędzie i jaka wersja VS?
Howard Hinnant
35

Ważna różnica między

class B {
    public:
    B(){}
    int i;
    int j;
};

i

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

oznacza, że ​​domyślny konstruktor zdefiniowany za pomocą B() = default;jest uważany za niezdefiniowany przez użytkownika . Oznacza to, że w przypadku inicjalizacji wartości, jak w

B* pb = new B();  // use of () triggers value-initialization

nastąpi specjalny rodzaj inicjalizacji, który w ogóle nie używa konstruktora, a dla typów wbudowanych spowoduje to zerową inicjalizację . W takim przypadku B(){}nie nastąpi. Zgodnie z normą C ++ n3337 § 8.5 / 7

Inicjalizacja wartości obiektu typu T oznacza:

- jeśli T jest (prawdopodobnie kwalifikowaną przez cv) typem klasy (klauzula 9) z konstruktorem dostarczonym przez użytkownika (12.1), wówczas wywoływany jest domyślny konstruktor dla T (a inicjalizacja jest źle sformułowana, jeśli T nie ma dostępnego domyślnego konstruktora );

- jeśli T jest (prawdopodobnie kwalifikowaną przez cv) klasą niezrzeszoną bez konstruktora dostarczonego przez użytkownika , to obiekt jest inicjowany przez zero i, jeśli domyślny konstruktor T zadeklarowany niejawnie jest nietrywialny, wywoływany jest ten konstruktor.

- jeśli T jest typem tablicowym, to każdy element jest inicjalizowany wartością; - w przeciwnym razie obiekt jest inicjalizowany przez zero.

Na przykład:

#include <iostream>

class A {
    public:
    A(){}
    int i;
    int j;
};

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

int main()
{
    for( int i = 0; i < 100; ++i) {
        A* pa = new A();
        B* pb = new B();
        std::cout << pa->i << "," << pa->j << std::endl;
        std::cout << pb->i << "," << pb->j << std::endl;
        delete pa;
        delete pb;
    }
  return 0;
}

możliwy wynik:

0,0
0,0
145084416,0
0,0
145084432,0
0,0
145084416,0
//...

http://ideone.com/k8mBrd

4pie0
źródło
Dlaczego więc „{}” i „= default” zawsze inicjują std :: string ideone.com/LMv5Uf ?
nawfel bgh
1
@nawfelbgh Domyślny konstruktor A () {} wywołuje domyślny konstruktor dla std :: string, ponieważ nie jest to typ POD. Domyślny ctor std :: string inicjuje go jako pusty łańcuch o rozmiarze 0. Domyślny wskaźnik dla skalarów nic nie robi: obiekty z automatycznym czasem trwania (i ich podobiekty) są inicjowane na nieokreślone wartości.
4pie0