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ę?
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ę?
Odpowiedzi:
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ć constexpr
i specyfikację wyjątku, aby dopasować niejawny konstruktor.
Używanie = default
przynosi 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.
constexpr
konstruktora (7.1.5), domyślnie zdefiniowany konstruktor domyślny jest constexpr
."
constexpr
taką, 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() {}
.
constexpr
(ponieważ element członkowski danych pozostaje niezainicjowany), a jego specyfikacja wyjątku dopuszcza wszystkie wyjątki. Wyjaśnię to jaśniej.
constexpr
któ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 s1
błąd, nie s2
. W obu clang i g ++.
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.
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łą
= default
w ogóle, a nie powody = default
na konstruktora vs. robi { }
.
{}
był już cechą języka przed wprowadzeniem =default
tych 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 ).
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 =default
w 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 =default
wyraż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 constexpr
dla domyślnych konstruktorów jest jednym z przykładów. Jest to po prostu łatwiejsze w użyciu psychicznie =default
niż konieczność oznaczania funkcji wszystkimi innymi specjalnymi słowami kluczowymi i takimi, które są implikowane =default
i 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.
= default
, a=0;
a nie było! Musiałem to rzucić na korzyść : a(0)
. Nadal nie wiem, jak przydatne = default
jest 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.
a=0
przykład wynika z zachowania trywialnych typów, które są osobnym (choć pokrewnym) tematem.
= default
i nadal a
będzie przyznawać =0
? w pewnym sensie? czy myślisz, że mógłbym stworzyć nowe pytanie typu "jak mieć konstruktora = default
i przyznać, że pola będą poprawnie zainicjalizowane?", btw miałem problem w a struct
a nie a class
, a aplikacja działa poprawnie nawet nie używam = default
, mogę dodaj minimalną strukturę do tego pytania, jeśli jest dobra :)
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).
Z powodu wycofania std::is_pod
i alternatywy std::is_trivial && std::is_standard_layout
fragment 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 Y
nadal ma standardowy układ.
default
nie jest nowym słowem kluczowym, jest po prostu nowym użyciem już zarezerwowanego słowa kluczowego.