To FAQ dotyczy agregatów i POD i obejmuje następujące materiały:
- Co to są agregaty ?
- Co to są POD (zwykłe stare dane)?
- W jaki sposób są one powiązane?
- Jak i dlaczego są wyjątkowe?
- Jakie zmiany w C ++ 11?
To FAQ dotyczy agregatów i POD i obejmuje następujące materiały:
Odpowiedzi:
Ten artykuł jest raczej długi. Jeśli chcesz wiedzieć zarówno o agregatach, jak i czujnikach POD (Plain Old Data), poświęć trochę czasu i przeczytaj je. Jeśli interesują Cię tylko agregaty, przeczytaj tylko pierwszą część. Jeśli jesteś zainteresowany tylko w strąkach potem trzeba najpierw przeczytać definicji, implikacje i przykłady kruszyw a następnie mogą przejść do POD ale ja i tak polecam czytać pierwszą część w całości. Pojęcie agregatów jest niezbędne do definiowania POD. Jeśli znajdziesz jakieś błędy (nawet drobne, w tym gramatykę, stylistykę, formatowanie, składnię itp.), Zostaw komentarz, będę edytować.
Ta odpowiedź dotyczy C ++ 03. Inne standardy C ++ patrz:
Formalna definicja ze standardu C ++ ( C ++ 03 8.5.1 § 1 ) :
Agregacja to tablica lub klasa (klauzula 9) bez konstruktorów zadeklarowanych przez użytkownika (12.1), bez prywatnych lub chronionych niestatycznych elementów danych (klauzula 11), bez klas podstawowych (klauzula 10) i bez funkcji wirtualnych (10.3 ).
OK, przeanalizujmy tę definicję. Przede wszystkim każda tablica jest agregacją. Klasa może być również agregacją, jeśli… czekaj! nic nie mówi się o strukturach lub związkach, czy nie mogą być agregacjami? Tak, moga. W C ++ termin class
odnosi się do wszystkich klas, struktur i związków. Tak więc klasa (lub struct lub union) jest agregatem tylko wtedy, gdy spełnia kryteria z powyższych definicji. Co oznaczają te kryteria?
Nie oznacza to, że klasa agregująca nie może mieć konstruktorów, w rzeczywistości może mieć domyślny konstruktor i / lub konstruktor kopii, o ile są one domyślnie zadeklarowane przez kompilator, a nie jawnie przez użytkownika
Brak prywatnych lub chronionych niestatycznych elementów danych . Możesz mieć dowolną liczbę prywatnych i chronionych funkcji składowych (ale nie konstruktorów), a także dowolną liczbę prywatnych lub chronionych statycznych elementów danych i funkcji składowych, jak chcesz i nie naruszać reguł dla klas agregujących
Klasa zagregowana może mieć zadeklarowany / zdefiniowany przez użytkownika operator przypisania kopii i / lub destruktor
Tablica jest agregatem, nawet jeśli jest tablicą typu klasy innej niż agregat.
Teraz spójrzmy na kilka przykładów:
class NotAggregate1
{
virtual void f() {} //remember? no virtual functions
};
class NotAggregate2
{
int x; //x is private by default and non-static
};
class NotAggregate3
{
public:
NotAggregate3(int) {} //oops, user-defined constructor
};
class Aggregate1
{
public:
NotAggregate1 member1; //ok, public member
Aggregate1& operator=(Aggregate1 const & rhs) {/* */} //ok, copy-assignment
private:
void f() {} // ok, just a private function
};
Masz pomysł. Zobaczmy teraz, jak agregaty są wyjątkowe. W przeciwieństwie do klas niezagregowanych można je inicjować za pomocą nawiasów klamrowych {}
. Ta składnia inicjalizacji jest powszechnie znana dla tablic i właśnie dowiedzieliśmy się, że są to agregaty. Zacznijmy od nich.
Type array_name[n] = {a1, a2, …, am};
if (m == n)
i- ty element tablicy jest inicjalizowany za pomocą i
else if (m <n)
pierwsze m elementów tablicy jest inicjalizowanych przez 1 , 2 ,…, mi pozostałen - m
elementy są, jeśli to możliwe, inicjowane wartością (wyjaśnienie terminu znajduje się poniżej)
, w przeciwnym razie, jeśli (m> n)
kompilator wygeneruje błąd w
innym przypadku (tak jest w przypadku, gdy n nie jest określone w ogóle int a[] = {1, 2, 3};
)
rozmiar przyjmuje się, że tablica (n) jest równa m, więcint a[] = {1, 2, 3};
jest równoważna zint a[3] = {1, 2, 3};
Gdy obiekt typu skalarną ( bool
, int
, char
, double
, wskaźniki, itp) jest wartość zainicjowany oznacza to, że jest inicjowana 0
dla tego typu ( false
o bool
, 0.0
o double
, etc.). Kiedy obiekt typu klasy z deklarowanym przez użytkownika domyślnym konstruktorem jest inicjowany wartością, wywoływany jest domyślny konstruktor. Jeśli domyślny konstruktor jest domyślnie zdefiniowany, wówczas wszystkie elementy niestatyczne są inicjowane rekurencyjnie pod względem wartości. Ta definicja jest nieprecyzyjna i nieco niepoprawna, ale powinna dać ci podstawowy pomysł. Nie można zainicjować wartości referencyjnej. Inicjalizacja wartości dla klasy niezagregowanej może się nie powieść, jeśli na przykład klasa nie ma odpowiedniego domyślnego konstruktora.
Przykłady inicjalizacji tablicy:
class A
{
public:
A(int) {} //no default constructor
};
class B
{
public:
B() {} //default constructor available
};
int main()
{
A a1[3] = {A(2), A(1), A(14)}; //OK n == m
A a2[3] = {A(2)}; //ERROR A has no default constructor. Unable to value-initialize a2[1] and a2[2]
B b1[3] = {B()}; //OK b1[1] and b1[2] are value initialized, in this case with the default-ctor
int Array1[1000] = {0}; //All elements are initialized with 0;
int Array2[1000] = {1}; //Attention: only the first element is 1, the rest are 0;
bool Array3[1000] = {}; //the braces can be empty too. All elements initialized with false
int Array4[1000]; //no initializer. This is different from an empty {} initializer in that
//the elements in this case are not value-initialized, but have indeterminate values
//(unless, of course, Array4 is a global array)
int array[2] = {1, 2, 3, 4}; //ERROR, too many initializers
}
Zobaczmy teraz, jak można zainicjować klasy agregujące za pomocą nawiasów klamrowych. Prawie w ten sam sposób. Zamiast elementów tablicy zainicjalizujemy niestatyczne elementy danych w kolejności ich pojawienia się w definicji klasy (wszystkie są z definicji publiczne). Jeśli jest mniej inicjatorów niż członków, reszta jest inicjowana wartością. Jeśli nie można zainicjować wartości jednego z elementów, które nie zostały jawnie zainicjowane, pojawia się błąd czasu kompilacji. Jeśli jest więcej inicjatorów, niż to konieczne, otrzymujemy również błąd czasu kompilacji.
struct X
{
int i1;
int i2;
};
struct Y
{
char c;
X x;
int i[2];
float f;
protected:
static double d;
private:
void g(){}
};
Y y = {'a', {10, 20}, {20, 30}};
W powyższym przykładzie y.c
jest inicjowana 'a'
, y.x.i1
z 10
, y.x.i2
z 20
, y.i[0]
z 20
, y.i[1]
z 30
, a y.f
ma wartość zainicjowany, to znaczy inicjowany 0.0
. Chroniony element statyczny d
w ogóle nie jest inicjowany, ponieważ tak jest static
.
Łączne związki różnią się tym, że możesz inicjować tylko ich pierwszego członka za pomocą nawiasów klamrowych. Myślę, że jeśli jesteś wystarczająco zaawansowany w C ++, aby nawet rozważyć użycie związków (ich użycie może być bardzo niebezpieczne i należy o tym ostrożnie pomyśleć), możesz sam sprawdzić zasady dotyczące związków w standardzie :).
Teraz, gdy wiemy, co jest specjalnego w agregatach, spróbujmy zrozumieć ograniczenia dotyczące klas; to znaczy, dlaczego tam są. Powinniśmy zrozumieć, że inicjalizacja członowa za pomocą nawiasów oznacza, że klasa jest niczym więcej niż sumą jej elementów. Jeśli obecny jest konstruktor zdefiniowany przez użytkownika, oznacza to, że użytkownik musi wykonać dodatkową pracę, aby zainicjować elementy, dlatego inicjacja nawiasów byłaby niepoprawna. Jeśli obecne są funkcje wirtualne, oznacza to, że obiekty tej klasy mają (w większości implementacji) wskaźnik do tak zwanej vtable klasy, która jest ustawiona w konstruktorze, więc inicjacja nawiasów nie byłaby wystarczająca. Resztę ograniczeń możesz znaleźć w podobny sposób jak ćwiczenie :).
Tyle o agregatach. Teraz możemy zdefiniować bardziej rygorystyczny zestaw typów, to znaczy POD
Formalna definicja ze standardu C ++ ( C ++ 03 9 §4 ) :
Struktura POD jest strukturą zagregowaną, która nie zawiera elementów niestatycznych typu non-POD-struct, non-POD-union (lub tablic takich typów) lub referencji i nie ma zdefiniowanego przez użytkownika operatora przypisania kopii i nie ma destruktor zdefiniowany przez użytkownika. Podobnie, związek POD jest związkiem zagregowanym, który nie ma elementów niestatycznych typu typu non-POD-struct, non-POD-union (lub tablica takich typów) lub odniesienia i nie ma operatora przypisania kopii zdefiniowanego przez użytkownika i brak niszczyciela zdefiniowanego przez użytkownika. Klasa POD to klasa będąca strukturą POD lub związkiem POD.
Wow, trudniej to przeanalizować, prawda? :) Pozostawmy związki (na tych samych podstawach, co powyżej) i sformułujmy je w nieco jaśniejszy sposób:
Klasa zagregowana jest nazywana POD, jeśli nie ma zdefiniowanego przez użytkownika operatora przypisania kopii i destruktora, a żaden z jej elementów niestatycznych nie jest klasą inną niż POD, tablicą non-POD ani referencją.
Co oznacza ta definicja? (Czy wspominałem o POD oznacza Plain Old Data ?)
Przykłady:
struct POD
{
int x;
char y;
void f() {} //no harm if there's a function
static std::vector<char> v; //static members do not matter
};
struct AggregateButNotPOD1
{
int x;
~AggregateButNotPOD1() {} //user-defined destructor
};
struct AggregateButNotPOD2
{
AggregateButNotPOD1 arrOfNonPod[3]; //array of non-POD class
};
Klasy POD, związki POD, typy skalarne i tablice takich typów są wspólnie nazywane typami POD.
POD są wyjątkowe na wiele sposobów. Podam tylko kilka przykładów.
Klasy POD są najbliższe strukturom C. W przeciwieństwie do nich POD mogą mieć funkcje składowe i dowolne elementy statyczne, ale żadne z tych dwóch nie zmienia układu pamięci obiektu. Więc jeśli chcesz napisać mniej lub bardziej przenośną bibliotekę dynamiczną, z której można korzystać z C, a nawet .NET, powinieneś spróbować, aby wszystkie wyeksportowane funkcje przyjmowały i zwracały tylko parametry typów POD.
Żywotność obiektów klasy innej niż POD rozpoczyna się po zakończeniu konstruktora, a kończy po zakończeniu działania destruktora. W przypadku klas POD żywotność rozpoczyna się, gdy pamięć jest zajęta, i kończy się, gdy pamięć zostanie zwolniona lub ponownie użyta.
W przypadku obiektów typu POD norma gwarantuje, że gdy memcpy
zawartość Twojego obiektu zostanie zapisana w tablicy znaków lub w postaci znaku bez znaku, a następnie memcpy
zawartość z powrotem w twoim obiekcie, obiekt zachowa swoją oryginalną wartość. Należy pamiętać, że nie ma takiej gwarancji na obiekty innych niż POD. Możesz także bezpiecznie kopiować obiekty POD za pomocą memcpy
. Poniższy przykład zakłada, że T jest typem POD:
#define N sizeof(T)
char buf[N];
T obj; // obj initialized to its original value
memcpy(buf, &obj, N); // between these two calls to memcpy,
// obj might be modified
memcpy(&obj, buf, N); // at this point, each subobject of obj of scalar type
// holds its original value
oświadczenie goto. Jak zapewne wiesz, nielegalne (kompilator powinien wydać błąd) jest przeskakiwanie za pomocą goto z miejsca, w którym jakaś zmienna nie była jeszcze w zasięgu, do punktu, w którym już jest w zasięgu. To ograniczenie ma zastosowanie tylko wtedy, gdy zmienna jest typu innego niż POD. W poniższym przykładzie f()
jest źle uformowany, podczas gdy g()
jest dobrze uformowany. Zauważ, że kompilator Microsoftu jest zbyt liberalny w stosunku do tej reguły - w obu przypadkach wydaje tylko ostrzeżenie.
int f()
{
struct NonPOD {NonPOD() {}};
goto label;
NonPOD x;
label:
return 0;
}
int g()
{
struct POD {int i; char c;};
goto label;
POD x;
label:
return 0;
}
Gwarantujemy, że na początku obiektu POD nie będzie paddingu. Innymi słowy, jeśli pierwszy element klasy POD klasy A jest typu T, możesz bezpiecznie przejść reinterpret_cast
od i uzyskać wskaźnik do pierwszego elementu i odwrotnie.A*
T*
Lista jest długa…
Ważne jest, aby zrozumieć, czym dokładnie jest POD, ponieważ wiele funkcji językowych, jak widzisz, zachowuje się dla nich inaczej.
private:
odpowiednio): struct A { int const a; };
wtedy A()
jest dobrze sformułowane, nawet jeśli A
domyślna definicja konstruktora byłaby źle sformułowana.
Standardowa definicja agregatu zmieniła się nieznacznie, ale nadal jest prawie taka sama:
Agregacja jest tablicą lub klasą (klauzula 9) bez konstruktorów dostarczonych przez użytkownika (12.1), bez inicjatorów nawiasowych lub równych dla niestatycznych elementów danych (9.2), bez prywatnych lub chronionych elementów niestatycznych ( Klauzula 11), brak klas podstawowych (Klauzula 10) i brak funkcji wirtualnych (10.3).
Ok, co się zmieniło?
Wcześniej agregacja nie mogła zawierać konstruktorów zadeklarowanych przez użytkownika , ale teraz nie może mieć konstruktorów podanych przez użytkownika . Czy jest jakaś różnica? Tak, ponieważ teraz możesz zadeklarować konstruktory i ustawić je domyślnie :
struct Aggregate {
Aggregate() = default; // asks the compiler to generate the default implementation
};
Jest to nadal agregacja, ponieważ konstruktor (lub dowolna specjalna funkcja członka), który jest domyślnie ustawiony w pierwszej deklaracji, nie jest dostarczany przez użytkownika.
Teraz agregacja nie może mieć żadnych inicjatorów nawiasowych lub równych dla niestatycznych elementów danych. Co to znaczy? To dlatego, że dzięki temu nowemu standardowi możemy inicjować członków bezpośrednio w klasie w następujący sposób:
struct NotAggregate {
int x = 5; // valid in C++11
std::vector<int> s{1,2,3}; // also valid
};
Użycie tej funkcji sprawia, że klasa nie jest już agregacją, ponieważ jest to w zasadzie odpowiednik dostarczenia własnego domyślnego konstruktora.
To, co jest agregatem, niewiele się zmieniło. To wciąż ten sam podstawowy pomysł, dostosowany do nowych funkcji.
POD przeszedł wiele zmian. Wiele wcześniejszych zasad dotyczących POD zostało złagodzonych w tym nowym standardzie, a sposób, w jaki podano definicję w normie, został radykalnie zmieniony.
Ideą POD jest uchwycenie zasadniczo dwóch różnych właściwości:
Z tego powodu definicja została podzielona na dwie odrębne koncepcje: klasy trywialne i klasy o standardowym układzie , ponieważ są one bardziej przydatne niż POD. Norma obecnie rzadko używa terminu POD, preferując bardziej szczegółowe pojęcia trywialne i standardowe .
Nowa definicja zasadniczo mówi, że POD jest klasą, która jest zarówno trywialna, jak i ma standardowy układ, a ta właściwość musi się rekurencyjnie przechowywać dla wszystkich niestatycznych elementów danych:
Struktura POD jest klasą niezwiązkową, która jest zarówno klasą trywialną, jak i klasą o układzie standardowym, i nie ma żadnych niestatycznych elementów danych typu non-POD struct, non-POD union (lub tablica takich typów). Podobnie, związek POD jest związkiem, który jest zarówno klasą trywialną, jak i standardową klasą układu i nie ma żadnych niestatycznych elementów danych typu non-POD struct, non-POD union (lub tablica takich typów). Klasa POD to klasa będąca strukturą POD lub związkiem POD.
Omówmy szczegółowo każdą z tych dwóch właściwości osobno.
Trivial to pierwsza właściwość wspomniana powyżej: trywialne klasy obsługują inicjalizację statyczną. Jeśli klasa jest trywialna do skopiowania (nadzbiór trywialnych klas), można skopiować jej reprezentację nad tym miejscem z rzeczami podobnymi memcpy
i oczekiwać, że wynik będzie taki sam.
Standard definiuje trywialną klasę w następujący sposób:
Trywialnie kopiowalna klasa to klasa, która:
- nie ma nietrywialnych konstruktorów kopii (12.8),
- nie ma nietrywialnych konstruktorów ruchów (12.8),
- nie ma nietrywialnych operatorów przypisywania kopii (13.5.3, 12.8),
- nie ma nietrywialnych operatorów przypisania ruchu (13.5.3, 12.8), oraz
- ma trywialny destruktor (12.4).
Klasa trywialna to klasa, która ma trywialny domyślny konstruktor (12.1) i jest trywialna do skopiowania.
[ Uwaga: W szczególności trywialnie dająca się skopiować lub trywialna klasa nie ma funkcji wirtualnych ani wirtualnych klas bazowych. —Wskazówka ]
Czym więc są te wszystkie trywialne i nietrywialne rzeczy?
Konstruktor kopiuj / przenieś dla klasy X jest trywialny, jeśli nie został dostarczony przez użytkownika i jeśli
- klasa X nie ma funkcji wirtualnych (10.3) ani wirtualnych klas bazowych (10.1), oraz
- konstruktor wybrany do kopiowania / przenoszenia każdego podobiektu klasy bezpośredniej jest trywialny, oraz
- dla każdego niestatycznego elementu danych X typu klasy (lub jego tablicy) konstruktor wybrany do kopiowania / przenoszenia tego elementu jest trywialny;
w przeciwnym razie konstruktor kopiowania / przenoszenia nie jest trywialny.
Zasadniczo oznacza to, że konstruktor kopiowania lub przenoszenia jest trywialny, jeśli nie jest udostępniony przez użytkownika, klasa nie ma w nim nic wirtualnego, a ta właściwość zachowuje się rekurencyjnie dla wszystkich członków klasy i klasy podstawowej.
Definicja trywialnego operatora kopiowania / przenoszenia jest bardzo podobna, po prostu zastępując słowo „konstruktor” słowem „operator przypisania”.
Trywialny destruktor ma również podobną definicję, z dodatkowym ograniczeniem, że nie może być wirtualny.
Istnieje jeszcze inna podobna reguła dla trywialnych domyślnych konstruktorów, z dodatkiem, że domyślny konstruktor nie jest trywialny, jeśli klasa ma niestatyczne elementy danych z inicjatorami nawiasów klamrowych lub równości , które widzieliśmy powyżej.
Oto kilka przykładów, aby wszystko wyczyścić:
// empty classes are trivial
struct Trivial1 {};
// all special members are implicit
struct Trivial2 {
int x;
};
struct Trivial3 : Trivial2 { // base class is trivial
Trivial3() = default; // not a user-provided ctor
int y;
};
struct Trivial4 {
public:
int a;
private: // no restrictions on access modifiers
int b;
};
struct Trivial5 {
Trivial1 a;
Trivial2 b;
Trivial3 c;
Trivial4 d;
};
struct Trivial6 {
Trivial2 a[23];
};
struct Trivial7 {
Trivial6 c;
void f(); // it's okay to have non-virtual functions
};
struct Trivial8 {
int x;
static NonTrivial1 y; // no restrictions on static members
};
struct Trivial9 {
Trivial9() = default; // not user-provided
// a regular constructor is okay because we still have default ctor
Trivial9(int x) : x(x) {};
int x;
};
struct NonTrivial1 : Trivial3 {
virtual void f(); // virtual members make non-trivial ctors
};
struct NonTrivial2 {
NonTrivial2() : z(42) {} // user-provided ctor
int z;
};
struct NonTrivial3 {
NonTrivial3(); // user-provided ctor
int w;
};
NonTrivial3::NonTrivial3() = default; // defaulted but not on first declaration
// still counts as user-provided
struct NonTrivial5 {
virtual ~NonTrivial5(); // virtual destructors are not trivial
};
Układ standardowy to druga właściwość. Standard wspomina, że są one przydatne do komunikowania się z innymi językami, a to dlatego, że klasa standardowego układu ma taki sam układ pamięci co odpowiednik struktury C lub unii.
Jest to kolejna właściwość, którą należy przechowywać rekurencyjnie dla członków i wszystkich klas podstawowych. Jak zwykle nie są dozwolone żadne funkcje wirtualne ani wirtualne klasy podstawowe. To spowodowałoby, że układ byłby niezgodny z C.
Zrelaksowana zasada polega na tym, że klasy o układzie standardowym muszą mieć wszystkie niestatyczne elementy danych z taką samą kontrolą dostępu. Wcześniej musiały być wszystkie publiczne , ale teraz możesz ustawić je jako prywatne lub chronione, o ile wszystkie są prywatne lub wszystkie chronione.
Podczas korzystania z dziedziczenia tylko jedna klasa w całym drzewie dziedziczenia może mieć niestatyczne elementy danych, a pierwszy niestatyczny element danych nie może być typu klasy podstawowej (może to złamać reguły aliasingu), w przeciwnym razie nie jest to standard- klasa układu.
Tak wygląda definicja w tekście standardowym:
Klasa z układem standardowym to klasa, która:
- nie ma elementów danych niestatycznych typu niestandardowego typu układu (lub tablicy takich typów) lub odniesienia,
- nie ma funkcji wirtualnych (10.3) ani wirtualnych klas bazowych (10.1),
- ma taką samą kontrolę dostępu (klauzula 11) dla wszystkich niestatycznych członków danych,
- nie ma klas bazowych o niestandardowym układzie,
- albo nie ma niestatycznych elementów danych w najbardziej pochodnej klasie i co najwyżej jednej klasy bazowej z niestatycznymi elementami danych, lub nie ma klas podstawowych z niestatycznymi elementami danych, oraz
- nie ma klas podstawowych tego samego typu, co pierwszy niestatyczny element danych.
Struktura standardowego układu to klasa standardowego układu zdefiniowana za pomocą struktury klucza klasy lub klasy klucza klasy.
Unia układu standardowego to klasa układu standardowego zdefiniowana za pomocą związku klucza klasy.
[ Uwaga: Klasy układu standardowego są przydatne do komunikacji z kodem napisanym w innych językach programowania. Ich układ jest określony w 9.2. —Wskazówka ]
Zobaczmy kilka przykładów.
// empty classes have standard-layout
struct StandardLayout1 {};
struct StandardLayout2 {
int x;
};
struct StandardLayout3 {
private: // both are private, so it's ok
int x;
int y;
};
struct StandardLayout4 : StandardLayout1 {
int x;
int y;
void f(); // perfectly fine to have non-virtual functions
};
struct StandardLayout5 : StandardLayout1 {
int x;
StandardLayout1 y; // can have members of base type if they're not the first
};
struct StandardLayout6 : StandardLayout1, StandardLayout5 {
// can use multiple inheritance as long only
// one class in the hierarchy has non-static data members
};
struct StandardLayout7 {
int x;
int y;
StandardLayout7(int x, int y) : x(x), y(y) {} // user-provided ctors are ok
};
struct StandardLayout8 {
public:
StandardLayout8(int x) : x(x) {} // user-provided ctors are ok
// ok to have non-static data members and other members with different access
private:
int x;
};
struct StandardLayout9 {
int x;
static NonStandardLayout1 y; // no restrictions on static members
};
struct NonStandardLayout1 {
virtual f(); // cannot have virtual functions
};
struct NonStandardLayout2 {
NonStandardLayout1 X; // has non-standard-layout member
};
struct NonStandardLayout3 : StandardLayout1 {
StandardLayout1 x; // first member cannot be of the same type as base
};
struct NonStandardLayout4 : StandardLayout3 {
int z; // more than one class has non-static data members
};
struct NonStandardLayout5 : NonStandardLayout3 {}; // has a non-standard-layout base class
Dzięki tym nowym regułom o wiele więcej typów może być teraz POD. I nawet jeśli typ nie jest POD, możemy skorzystać z niektórych właściwości POD osobno (jeśli jest to tylko trywialny lub standardowy układ).
Biblioteka standardowa ma cechy do testowania tych właściwości w nagłówku <type_traits>
:
template <typename T>
struct std::is_pod;
template <typename T>
struct std::is_trivial;
template <typename T>
struct std::is_trivially_copyable;
template <typename T>
struct std::is_standard_layout;
Możemy odwoływać się do standardu Draft C ++ 14 w celach informacyjnych.
Jest to omówione w sekcji 8.5.1
Agregaty, która podaje następującą definicję:
Agregacja to tablica lub klasa (klauzula 9) bez konstruktorów podanych przez użytkownika (12.1), bez prywatnych lub chronionych elementów niestatycznych danych (klauzula 11), bez klas podstawowych (klauzula 10) i bez funkcji wirtualnych (10.3 ).
Jedyną zmianą jest teraz dodanie inicjatorów elementów w klasie , które nie powodują, że klasa nie jest agregacją. Więc poniższym przykładzie z c ++ 11 zagregowanego inicjalizacji dla klas z członkiem w tempie-inicjalizatorów :
struct A
{
int a = 3;
int b = 3;
};
nie był agregatem w C ++ 11, ale jest w C ++ 14. Ta zmiana jest uwzględniona w N3605: Inicjatory składowe i agregaty , które mają następujące streszczenie:
Bjarne Stroustrup i Richard Smith podnieśli problem związany z inicjalizacją agregacji i niedziałaniem inicjatorów składowych. W tym artykule zaproponowano rozwiązanie tego problemu poprzez przyjęcie proponowanego sformułowania Smitha, które usuwa ograniczenie, że agregaty nie mogą mieć inicjatorów składowych.
Definicja struktury POD ( zwykłe stare dane ) została omówiona w sekcji 9
Klasy, która mówi:
Struktura POD 110 jest klasą niezwiązkową, która jest zarówno klasą trywialną, jak i klasą z układem standardowym i nie ma żadnych niestatycznych elementów danych typu non-POD struct, non-POD union (lub tablica takich typów). Podobnie, związek POD jest związkiem, który jest zarówno klasą trywialną, jak i klasą o układzie standardowym i nie ma żadnych niestatycznych elementów danych typu non-POD struct, non-POD union (lub tablica takich typów). Klasa POD to klasa będąca strukturą POD lub związkiem POD.
który jest taki sam jak C ++ 11.
Jak zauważono w komentarzach, pod opiera się na definicji standardowego układu, która zmieniła się dla C ++ 14, ale stało się tak dzięki raportom błędów, które zostały zastosowane do C ++ 14 po fakcie.
Były trzy DR:
Tak więc standardowy układ wyszedł z tego Pre C ++ 14:
Klasa z układem standardowym to klasa, która:
- (7.1) nie ma elementów danych niestatycznych typu niestandardowego układu klasy (lub tablicy takich typów) lub odniesienia,
- (7.2) nie ma funkcji wirtualnych ([class.virtual]) ani wirtualnych klas bazowych ([class.mi]),
- (7.3) ma taką samą kontrolę dostępu (klauzula [class.access]) dla wszystkich niestatycznych elementów danych,
- (7.4) nie ma klas bazowych o niestandardowym układzie,
- (7.5) albo nie ma niestatycznych elementów danych w najbardziej pochodnej klasie i co najwyżej jednej klasy bazowej z niestatycznymi elementami danych, albo nie ma klas podstawowych z niestatycznymi elementami danych, i
- (7.6) nie ma klas podstawowych tego samego typu, co pierwszy element danych niestatycznych. 109
Do tego w C ++ 14 :
Klasa S jest klasą układu standardowego, jeżeli:
- (3.1) nie ma elementów danych niestatycznych typu niestandardowego układu klasy (lub tablicy takich typów) lub odniesienia,
- (3.2) nie ma wirtualnych funkcji i wirtualnych klas bazowych,
- (3.3) ma taką samą kontrolę dostępu dla wszystkich niestatycznych elementów danych,
- (3.4) nie ma klas bazowych o niestandardowym układzie,
- (3.5) ma co najwyżej jeden podobiekt klasy bazowej dowolnego danego typu,
- (3.6) ma wszystkie niestatyczne elementy danych i pola bitowe w klasie, a jej klasy podstawowe najpierw zadeklarowane w tej samej klasie, oraz
- (3.7) nie ma elementu zestawu M (S) typów jako klasy bazowej, gdzie dla dowolnego typu X, M (X) jest zdefiniowany w następujący sposób.104 [Uwaga: M (X) jest zbiorem typów wszystkie podobiekty klasy innej niż podstawowa, które mogą mieć przesunięcie zerowe w X. - uwaga końcowa]
- (3.7.1) Jeśli X jest niekluczowym typem klasy bez żadnych (prawdopodobnie odziedziczonych) elementów niestatycznych danych, zestaw M (X) jest pusty.
- (3.7.2) Jeżeli X jest niekluczowym typem klasy z niestatycznym składnikiem danych typu X0, który ma albo zerowy rozmiar, albo jest pierwszym niestatycznym składnikiem danych X (przy czym ten człon może być anonimowym związkiem ), zestaw M (X) składa się z X0 i elementów M (X0).
- (3.7.3) Jeśli X jest typem unii, zbiór M (X) jest sumą wszystkich M (Ui) i zbiorem zawierającym wszystkie Ui, gdzie każdy Ui jest typem i-tego niestatycznego elementu danych X .
- (3.7.4) Jeśli X jest typem tablicy z elementem typu Xe, zbiór M (X) składa się z Xe i elementów M (Xe).
- (3.7.5) Jeśli X jest nieklasowym typem niebędącym tablicą, zestaw M (X) jest pusty.
czy możesz opracować następujące zasady:
Spróbuję:
a) klasy o układzie standardowym muszą mieć wszystkie elementy danych niestatycznych z taką samą kontrolą dostępu
To proste: wszystkie non-statyczne członkowie danych musi wszystko być public
, private
albo protected
. Nie możesz mieć trochępublic
niektórych private
.
Ich uzasadnienie odnosi się do rozumowania, że w ogóle rozróżnia się „układ standardowy” i „układ niestandardowy”. Mianowicie, aby dać kompilatorowi swobodę wyboru sposobu zapisania rzeczy w pamięci. Nie chodzi tylko o wskaźniki vtable.
Kiedy znormalizowali C ++ w 98, musieli w zasadzie przewidzieć, w jaki sposób ludzie będą go implementować. Chociaż mieli sporo doświadczenia w implementacji różnych wersji C ++, nie byli pewni co do rzeczy. Postanowili więc zachować ostrożność: dać kompilatorom jak najwięcej swobody.
Dlatego definicja POD w C ++ 98 jest tak ścisła. Dało to kompilatorom C ++ dużą swobodę w zakresie układu elementów dla większości klas. Zasadniczo typy POD miały być szczególnymi przypadkami, czymś specjalnie napisanym z konkretnego powodu.
Podczas pracy nad C ++ 11 mieli dużo więcej doświadczenia z kompilatorami. I zdali sobie sprawę, że ... autorzy kompilatorów C ++ są naprawdę leniwi. Mieli całą tę wolność, ale nic z tym nie zrobili .
Reguły standardowego układu są mniej więcej kodyfikującą powszechną praktyką: większość kompilatorów tak naprawdę wcale nie musiała dużo zmieniać, aby cokolwiek zaimplementować (poza pewnymi rzeczami dla odpowiednich cech typu).
Teraz, jeśli chodzi o public
/ private
, rzeczy są inne. Swoboda zmiany kolejności członków, którzy są public
vs.private
rzeczywistymi może mieć znaczenie dla kompilatora, szczególnie przy debugowaniu kompilacji. A ponieważ standardowym układem jest kompatybilność z innymi językami, nie można mieć innego układu w debugowaniu niż w wydaniu.
Jest też fakt, że tak naprawdę nie zaszkodzi to użytkownikowi. Jeśli tworzysz klasę zamkniętą, szanse są duże, że i tak wszyscy twoi członkowie danych będą private
. Zasadniczo nie ujawniasz publicznych członków danych w pełni enkapsulowanych typach. Byłby to więc problem tylko dla tych nielicznych użytkowników, którzy chcą to zrobić i chcą tego podziału.
Więc to nie jest wielka strata.
b) tylko jedna klasa w całym drzewie dziedziczenia może mieć niestatycznych członków danych,
Powodem tego jest to, dlaczego ponownie znormalizowali standardowy układ: powszechna praktyka.
Nie ma powszechnej praktyki, jeśli chodzi o posiadanie dwóch członków drzewa spadkowego, które faktycznie przechowują rzeczy. Niektórzy stawiają klasę podstawową przed pochodną, inni robią to w drugą stronę. W jaki sposób zamawiasz członków, jeśli pochodzą z dwóch klas podstawowych? I tak dalej. Kompilatory różnią się znacznie w tych kwestiach.
Ponadto, dzięki zasadzie zero / one / infinity, kiedy powiesz, że możesz mieć dwie klasy z członkami, możesz powiedzieć tyle, ile chcesz. Wymaga to dodania wielu reguł układu, jak sobie z tym poradzić. Musisz powiedzieć, jak działa wielokrotne dziedziczenie, które klasy umieszczają swoje dane przed innymi klasami itp. To wiele zasad, przy bardzo niewielkim zysku materialnym.
Nie możesz zrobić wszystkiego, co nie ma funkcji wirtualnych i domyślnego standardowego układu konstruktora.
a pierwszy niestatyczny element danych nie może być typu klasy podstawowej (może to złamać reguły aliasingu).
Naprawdę nie mogę z tym rozmawiać. Nie jestem wystarczająco wykształcony w zasadach aliasingu C ++, aby naprawdę to zrozumieć. Ma to jednak coś wspólnego z faktem, że element podstawowy będzie miał ten sam adres co sama klasa podstawowa. To jest:
struct Base {};
struct Derived : Base { Base b; };
Derived d;
static_cast<Base*>(&d) == &d.b;
I prawdopodobnie jest to sprzeczne z zasadami aliasingu C ++. W pewnym sensie.
Uważa jednak, że w ten sposób: jak użyteczne mogłoby posiadające zdolność, aby to zrobić kiedykolwiek rzeczywiście było? Ponieważ tylko jedna klasa może mieć niestatyczne elementy danych, Derived
musi to być ta klasa (ponieważ ma ona Base
jako element członkowski). Więc Base
musi być pusty (danych). A jeśli Base
jest pusty, a także klasa podstawowa ... dlaczego w ogóle ma go członek danych?
Ponieważ Base
jest pusty, nie ma stanu. Tak więc wszelkie niestatyczne funkcje składowe wykonają to, co robią na podstawie swoich parametrów, a nie this
wskaźnika.
Więc znowu: bez dużej straty.
static_cast<Base*>(&d)
i &d.b
są tego samego Base*
typu, wskazują na różne rzeczy, łamiąc w ten sposób zasadę aliasingu. Proszę mnie poprawić.
Derived
musi to być ta klasa?
Derived
pierwszy element członkowski był klasą podstawową, musi mieć dwie rzeczy: klasę podstawową i element członkowski . A ponieważ tylko jedna klasa w hierarchii może mieć elementy (i nadal mieć układ standardowy), oznacza to, że jej klasa podstawowa nie może mieć elementów.
Pobierz końcowy projekt standardu międzynarodowego C ++ 17 International tutaj .
Kruszywa
C ++ 17 rozszerza i usprawnia agregowanie i inicjalizację agregatów. Biblioteka standardowa zawiera teraz także std::is_aggregate
klasę cech typu. Oto formalna definicja z sekcji 11.6.1.1 i 11.6.1.2 (wybrane odniesienia wewnętrzne):
Agregacja to tablica lub klasa
bez - dostarczonych przez użytkownika, jawnych lub odziedziczonych konstruktorów,
- bez prywatnych lub chronionych niestatycznych elementów danych,
- bez funkcji wirtualnych i
- bez wirtualnych, prywatnych lub chronionych klas podstawowych.
[Uwaga: Inicjalizacja zagregowana nie pozwala na dostęp do chronionych i prywatnych członków lub konstruktorów klasy podstawowej. —Wskazówka]
Elementami agregacji są:
- dla tablicy elementy tablicy w porządku rosnącym indeksu dolnego lub
- dla klasy bezpośrednie klasy podstawowe w kolejności deklaracji, po których następują bezpośrednie niestatyczne elementy danych, które nie są członkowie anonimowego związku w kolejności deklaracji.
Co się zmieniło?
struct B1 // not a aggregate
{
int i1;
B1(int a) : i1(a) { }
};
struct B2
{
int i2;
B2() = default;
};
struct M // not an aggregate
{
int m;
M(int a) : m(a) { }
};
struct C : B1, B2
{
int j;
M m;
C() = default;
};
C c { { 1 }, { 2 }, 3, { 4 } };
cout
<< "is C aggregate?: " << (std::is_aggregate<C>::value ? 'Y' : 'N')
<< " i1: " << c.i1 << " i2: " << c.i2
<< " j: " << c.j << " m.m: " << c.m.m << endl;
//stdout: is C aggregate?: Y, i1=1 i2=2 j=3 m.m=4
struct D // not an aggregate
{
int i = 0;
D() = default;
explicit D(D const&) = default;
};
struct B1
{
int i1;
B1() : i1(0) { }
};
struct C : B1 // not an aggregate
{
using B1::B1;
};
Trywialne klasy
Definicja klasy trywialnej została przerobiona w C ++ 17, aby rozwiązać kilka błędów, które nie zostały usunięte w C ++ 14. Zmiany miały charakter techniczny. Oto nowa definicja w wersji 12.0.6 (z odniesieniem do wewnętrznych odniesień):
Trywialnie kopiowalna klasa to klasa:
- w której każdy konstruktor kopiujący, konstruktor przenoszenia, operator przypisania kopii i operator przypisania przeniesienia jest albo usunięty, albo trywialny,
- który ma co najmniej jeden nieusunięty konstruktor kopii, konstruktor przeniesienia, operator przypisania kopii, lub przenieść operator przypisania, i
- który ma trywialny, nieskasowany destruktor.
Klasa trywialna to klasa, którą można w trywialny sposób kopiować i ma jeden lub więcej domyślnych konstruktorów, z których wszystkie są albo trywialne, albo usunięte, a przynajmniej jedna z nich nie jest usuwana. [Uwaga: W szczególności klasa, którą można w prosty sposób kopiować lub trywialna, nie ma funkcji wirtualnych ani wirtualnych klas bazowych. - uwaga końcowa]
Zmiany:
std::memcpy
. Była to sprzeczność semantyczna, ponieważ definiując jako usunięte wszystkie operatory konstruktora / przypisania, twórca klasy wyraźnie chciał, aby klasa nie mogła być kopiowana / przenoszona, ale klasa nadal spełniała definicję klasy, którą można w prosty sposób skopiować. Dlatego w C ++ 17 mamy nową klauzulę stwierdzającą, że trywialnie dająca się skopiować klasa musi mieć co najmniej jeden trywialny, nieskasowany (choć niekoniecznie publicznie dostępny) konstruktor / operator przenoszenia / przypisania. Patrz N4148 , DR1734Klasy w układzie standardowym
Zmodyfikowano także definicję standardowego układu, aby uwzględnić raporty błędów. Ponownie zmiany miały charakter techniczny. Oto tekst ze standardu (12.0.7). Tak jak poprzednio, wybierane są odniesienia wewnętrzne:
Klasa S klasy standardowego układu, jeżeli:
- nie ma nie-statycznych pól grupy typu nie-standardowego układu (lub szeregu takich typów) lub odniesienia,
- nie ma funkcji wirtualnych i nie klas wirtualnych bazowe
- ma taką samą kontrolę dostępu dla wszystkich niestatycznych elementów danych,
- nie ma klas bazowych o niestandardowym układzie,
- ma co najwyżej jeden podobiekt klasy bazowej dowolnego określonego typu,
- ma wszystkie niestatyczne elementy danych i pola bitowe w klasa i jej klasy podstawowe zadeklarowane po raz pierwszy w tej samej klasie, i
- nie ma elementu zestawu M (S) typów (zdefiniowanych poniżej) jako klasa podstawowa. 108
M (X) jest zdefiniowana w następujący sposób:
- Jeśli X jest niekluczowym typem klasy bez żadnych (prawdopodobnie odziedziczonych) elementów niestatycznych danych, zestaw M (X) jest pusty.
- Jeśli X jest typem klasy nie-unii, której pierwszy niestatyczny element danych ma typ X0 (gdzie ten element może być anonimowym związkiem), zestaw M (X) składa się z X0 i elementów M (X0).
- Jeśli X jest typem unii, zbiór M (X) jest sumą wszystkich M (Ui) i zestawem zawierającym wszystkie Ui, gdzie każdy Ui jest typem i-tego niestatycznego elementu danych X.
- Jeśli X jest typem tablicy z typem elementu Xe, zestaw M (X) składa się z Xe i elementów M (Xe).
- Jeśli X jest nieklasowym typem niebędącym tablicą, zestaw M (X) jest pusty.
[Uwaga: M (X) jest zbiorem typów wszystkich podobiektów spoza klasy podstawowej, które są gwarantowane w klasie układu standardowego z zerowym przesunięciem w X. - uwaga końcowa]
[Przykład:
—Przykład]struct B { int i; }; // standard-layout class struct C : B { }; // standard-layout class struct D : C { }; // standard-layout class struct E : D { char : 4; }; // not a standard-layout class struct Q {}; struct S : Q { }; struct T : Q { }; struct U : S, T { }; // not a standard-layout class
108) Zapewnia to, że dwa podobiekty, które mają ten sam typ klasy i należą do tego samego najbardziej pochodnego obiektu, nie są przydzielane pod tym samym adresem.
Zmiany:
Uwaga: Komitet ds. Standardów C ++ zamierzał zastosować powyższe zmiany w oparciu o raporty błędów do C ++ 14, chociaż nowego języka nie ma w opublikowanym standardzie C ++ 14. Jest w standardzie C ++ 17.
Zgodnie z resztą jasnego tematu tego pytania, znaczenie i zastosowanie agregatów zmienia się z każdym standardem. Istnieje kilka kluczowych zmian na horyzoncie.
W C ++ 17 ten typ jest nadal agregacją:
struct X {
X() = delete;
};
A zatem X{}
nadal się kompiluje, ponieważ jest to inicjalizacja zbiorcza, a nie wywołanie konstruktora. Zobacz także: Kiedy prywatny konstruktor nie jest prywatnym konstruktorem?
W C ++ 20 ograniczenie zmieni się z wymagania:
brak podanych przez użytkownika
explicit
lub odziedziczonych konstruktorów
do
brak deklarowanych lub dziedziczonych konstruktorów
Zostało to przyjęte w roboczym projekcie C ++ 20 . Ani X
tutaj, ani C
w powiązanym pytaniu nie będą agregowane w C ++ 20.
Daje to również efekt jo-jo w następującym przykładzie:
class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};
W C ++ 11/14, B
była nie agregat ze względu na klasy bazowej, więc B{}
wykonuje wartości inicjalizacji, który wzywa B::B()
, który wzywa A::A()
, w miejscu, gdzie jest ona dostępna. To było dobrze uformowane.
W C ++ 17 B
stał się agregatorem, ponieważ dozwolone były klasy podstawowe, co spowodowało B{}
inicjalizację agregacji. Wymaga to inicjalizacji listy kopii A
z {}
, ale spoza kontekstu B
, w którym nie jest dostępny. W C ++ 17 jest to źle sformułowane ( auto x = B();
byłoby dobrze).
W C ++ 20 teraz, z powodu powyższej zmiany reguły, B
po raz kolejny przestaje być agregacją (nie z powodu klasy podstawowej, ale z powodu deklarowanego przez użytkownika domyślnego konstruktora - nawet jeśli jest on domyślny). Wracamy do B
konstruktora i ten fragment kodu jest dobrze uformowany.
Często pojawiającym się problemem jest konieczność użycia emplace()
konstruktorów w stylu z agregacjami:
struct X { int a, b; };
std::vector<X> xs;
xs.emplace_back(1, 2); // error
To nie działa, ponieważ emplace
spróbuje skutecznie wykonać inicjalizację X(1, 2)
, co jest nieprawidłowe. Typowym rozwiązaniem jest dodanie konstruktora X
, ale dzięki tej propozycji (obecnie działającej w Core) agregaty będą skutecznie syntetyzowały konstruktory, które postępują właściwie i zachowują się jak zwykłe konstruktory. Powyższy kod zostanie skompilowany w stanie obecnym w C ++ 20.
W C ++ 17 nie kompiluje się:
template <typename T>
struct Point {
T x, y;
};
Point p{1, 2}; // error
Użytkownicy musieliby napisać własny przewodnik po dedukcji dla wszystkich szablonów agregujących:
template <typename T> Point(T, T) -> Point<T>;
Ale ponieważ jest to w pewnym sensie „oczywista rzecz” do zrobienia i jest to po prostu bojler, język zrobi to za Ciebie. Ten przykład skompiluje się w C ++ 20 (bez potrzeby dostarczonego przez użytkownika przewodnika po dedukcji).