C++ data alignment /member order & inheritance

Jak członkowie danych są wyrównywani / porządkowani, jeśli używane jest dziedziczenie / dziedziczenie wielokrotne? Czy ten kompilator jest specyficzny?

Czy istnieje sposób, aby określić w klasie pochodnej, w jaki sposób członkowie (w tym Członkowie z klasy bazowej) mają być uporządkowani / wyrównani?

Dzięki!

Author: linuxbuild, 2010-01-05

8 answers

Naprawdę zadajesz wiele różnych pytań, więc postaram się odpowiedzieć na każde po kolei.

Najpierw chcesz wiedzieć, jak członkowie danych są wyrównane. Wyrównanie członów jest definiowane przez kompilator, ale ze względu na sposób, w jaki Procesory radzą sobie z nieprawidłowymi danymi, wszystkie mają tendencję do podążania za tym samym

Wytyczne, że struktury powinny być wyrównane w oparciu o najbardziej restrykcyjny element (który jest zwykle, ale nie zawsze, największym typem wewnętrznym), a struktury są zawsze wyrównane tak, że wszystkie elementy tablicy są wyrównane tak samo.

Na przykład:

struct some_object
{
    char c;
    double d;
    int i;
};

Ta struktura będzie miała 24 bajty. Ponieważ klasa zawiera podwójne, będzie ona wyrównana o 8 bajtów, co oznacza, że znak będzie wyrównany o 7 bajtów, A int będzie wyrównany o 4, aby zapewnić, że w tablicy some_object wszystkie elementy będą wyrównane o 8 bajtów. Ogólnie rzecz biorąc, jest to zależne od kompilatora, chociaż można zauważyć, że dla danej architektury procesora większość kompilatorów wyrównuje dane to samo.

Druga rzecz, o której wspomniałeś, to pochodne Członkowie klasy. Uporządkowanie i wyrównanie klas pochodnych jest trochę uciążliwe. Klasy indywidualnie postępują zgodnie z zasadami opisanymi powyżej dla struktur, ale kiedy zaczynasz mówić o dziedziczeniu, dostajesz brudną murawę. Podano następujące klasy:

class base
{
    int i;
};

class derived : public base // same for private inheritance
{
    int k;
};

class derived2 : public derived
{
    int l;
};

class derived3 : public derived, public derived2
{
    int m;
};

class derived4 : public virtual base
{
    int n;
};

class derived5 : public virtual base
{
    int o;
};

class derived6 : public derived4, public derived5
{
    int p;
};

Układ pamięci dla bazy będzie następujący:

int i // base

Układ pamięci dla pochodnej będzie:

int i // base
int k // derived

Układ pamięci dla derived2 będzie be:

int i // base
int k // derived
int l // derived2

Układ pamięci dla derived3 będzie następujący:

int i // base
int k // derived
int i // base
int k // derived
int l // derived2
int m // derived3

Można zauważyć, że baza i pochodna pojawiają się tu dwa razy. To jest cud wielokrotnego dziedziczenia.

Aby obejść, że mamy wirtualne dziedzictwo.

Układ pamięci dla derived4 będzie następujący:

base* base_ptr // ptr to base object
int n // derived4
int i // base

Układ pamięci dla derived5 będzie następujący:

base* base_ptr // ptr to base object
int o // derived5
int i // base

Układ pamięci dla derived6 będzie następujący:

base* base_ptr // ptr to base object
int n // derived4
int o // derived5
int i // base

Zauważ, że pochodne 4, 5 i 6 mają wskaźnik do obiektu podstawowego. Jest to konieczne, aby podczas wywoływania którejkolwiek z funkcji bazy miał obiekt do przekazania tym funkcjom. Struktura ta jest zależna od kompilatora, ponieważ nie jest określona w specyfikacji języka, ale prawie wszystkie Kompilatory implementują ją tak samo.

Sprawy stają się bardziej skomplikowane, gdy zaczyna się mówić o funkcjach wirtualnych, ale znowu, większość kompilatorów implementuje je tak samo. Weź udział w następujących zajęciach:

class vbase
{
    virtual void foo() {};
};

class vbase2
{
    virtual void bar() {};
};

class vderived : public vbase
{
    virtual void bar() {};
    virtual void bar2() {};
};

class vderived2 : public vbase, public vbase2
{
};

Każda z tych klas zawiera co najmniej jedną funkcję wirtualną.

Układ pamięci dla vbase będzie następujący:

void* vfptr // vbase

Układ pamięci dla vbase2 będzie następujący:

void* vfptr // vbase2

Układ pamięci dla vderived będzie:

void* vfptr // vderived

Układ pamięci dla vderived2 będzie następujący:

void* vfptr // vbase
void* vfptr // vbase2

Jest wiele rzeczy, których ludzie nie rozumieją, jak działają vftables. Pierwszą rzeczą do zrozumienia jest to, że klasy przechowują tylko wskaźniki do vftables, a nie całe vftables.

Co to oznacza że bez względu na to, ile funkcji wirtualnych posiada klasa, będzie miała tylko jedną funkcję vftable, chyba że dziedziczy ją z innego miejsca poprzez dziedziczenie wielokrotne. Prawie wszystkie Kompilatory umieszczają wskaźnik vftable przed resztą członków klasy. Oznacza to, że możesz mieć pewne wypełnienie między wskaźnikiem vftable a członkami klasy.

Mogę też powiedzieć, że prawie wszystkie Kompilatory implementują możliwości pakietu pragma, które pozwalają ręcznie wymusić strukturę / align = "left" / Generalnie nie chcesz tego robić, chyba że naprawdę wiesz, co robisz, ale to tam jest, a czasami jest konieczne.

Ostatnią rzeczą, o którą zapytałeś, jest to, czy możesz kontrolować zamawianie. Zawsze kontrolujesz zamawianie. Kompilator będzie zawsze porządkował rzeczy w kolejności, w jakiej je zapisujesz. Mam nadzieję, że to długie Wyjaśnienie trafi we wszystko, co musisz wiedzieć.
 59
Author: Beanz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-05 17:48:54

To nie tylko specyficzne dla kompilatora - prawdopodobnie będą na niego wpływ opcje kompilatora. Nie znam żadnych kompilatorów, które dają drobnoziarnistą kontrolę nad tym, jak członkowie i Bazy są pakowane i porządkowane z wieloma dziedziczeniami.

Jeśli robisz coś, co polega na zamówieniu i pakowaniu, spróbuj zapisać strukturę POD wewnątrz klasy i użyć jej.

 3
Author: Joe Gauterin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-05 14:21:44

Jest specyficzny dla kompilatora.

Edit: zasadniczo sprowadza się to do miejsca, w którym znajduje się wirtualna tabela, a to może być różne w zależności od tego, który kompilator jest używany.

 1
Author: Goz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-05 14:17:30

Jak tylko twoja klasa nie będzie POD (zwykłe stare dane), wszystkie zakłady są wyłączone. Prawdopodobnie istnieją specyficzne dla kompilatora dyrektywy, których możesz użyć do spakowania / wyrównania danych.

 1
Author: James,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-05 14:17:32

Kompilatory zazwyczaj wyrównują elementy danych w strukturach, aby umożliwić łatwy dostęp. Oznacza to, że elementy danych zwykle zaczynają się od granic słów, a luki te zwykle pozostaną w strukturze, aby zapewnić, że granice słów nie są okraszone.

Więc

struct foo
{
    char a;
    int b;
    char c;
}

Zwykle zajmuje więcej niż 6 bajtów dla 32-bitowej maszyny

Klasa bazowa jest zwykle ustawiona jako pierwsza, a klasa pochodna, którą ustawiono za klasą bazową. Pozwala to adresowi klasy bazowej na równy adres pochodnej klasy.

W dziedziczeniu wielokrotnym istnieje przesunięcie między adresem klasy A adresem drugiej klasy bazowej. >static_cast i dynamic_cast obliczą przesunięcie. Nie. Odlewy w stylu C wykonują odlew statyczny, jeśli to możliwe, w przeciwnym razie reinterpretują odlew.

Jak inni wspominali, wszystko to jest specyficzne dla kompilatora, ale powyższe powinno dać ci przybliżony przewodnik po tym, co zwykle się dzieje.

 1
Author: doron,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-05 14:27:06

Kolejność obiektów w dziedziczeniu wielokrotnym nie zawsze jest określona. Z tego, co doświadczyłem, kompilator użyje określonej kolejności, chyba że nie może. nie może użyć określonej kolejności, gdy pierwsza klasa bazowa nie ma funkcji wirtualnych, a inna klasa bazowa ma funkcje wirtualne. W tym przypadku, pierwsze bajty klasy muszą być wskaźnikiem tabeli funkcji wirtualnych, ale pierwsza klasa bazowa nie ma takiego wskaźnika. Kompilator przestawia klasy bazowe tak, aby pierwszy z nich posiada wirtualny wskaźnik tabeli funkcji.

Przetestowałem to zarówno z msdev i G++ i oba z nich przearanżować klasy. Irytujące wydaje się, że mają inne zasady, jak to robią. Jeśli masz 3 lub więcej klas bazowych, a pierwsza z nich nie ma funkcji wirtualnych, Kompilatory te będą miały różne układy.

Aby być bezpiecznym, wybierz dwa i unikaj drugiego.

  1. Nie polegaj na porządkowaniu klas bazowych, gdy używasz wielu spadek.

  2. Podczas używania dziedziczenia wielokrotnego, umieść wszystkie klasy bazowe z funkcjami wirtualnymi przed klasami bazowymi bez funkcji wirtualnych.

  3. Używaj 2 lub mniej klas bazowych (ponieważ Kompilatory zmieniają układ w ten sam sposób w tym przypadku)

 1
Author: mccoyn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-27 19:17:34

Wszystkie Kompilatory, które znam, umieszczają obiekt klasy bazowej przed członkami danych w obiekcie klasy pochodnej. Członkowie danych są w kolejności podanej w deklaracji klasy. Mogą być luki z powodu wyrównania. Nie mówię, że tak musi być.

 0
Author: user231967,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-05 14:25:31

Mogę odpowiedzieć na jedno z pytań.

Jak członkowie danych są wyrównywani / porządkowani, jeśli używane jest dziedziczenie / dziedziczenie wielokrotne?

Stworzyłem narzędzie do wizualizacji układu pamięci klas, ramek stosów funkcji i innych informacji ABI (Linux, GCC). Możesz spojrzeć na wynik dla mysqlpp:: Connection class (inherits OptionalExceptions) z biblioteki MySQL++ tutaj.

Tutaj wpisz opis obrazka

 0
Author: linuxbuild,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-12 21:05:59