Dlaczego optymalizacja pustej bazy jest zabroniona, skoro pusta klasa podstawowa jest również zmienną składową?


14

Optymalizacja pustej bazy jest świetna. Obejmuje jednak następujące ograniczenie:

Optymalizacja pustej bazy jest zabroniona, jeśli jedna z pustych klas bazowych jest również typem lub bazą typu pierwszego niestatycznego elementu danych, ponieważ dwa podstawowe podobiekty tego samego typu muszą mieć różne adresy w reprezentacji obiektu najbardziej pochodnego typu.

Aby wyjaśnić to ograniczenie, rozważ następujący kod. Nie static_assertpowiedzie się. Natomiast zmiana albo dziedziczenia Fooalbo Barzamiast Base2spowoduje uniknięcie błędu:

#include <cstddef>

struct Base  {};
struct Base2 {};

struct Foo : Base {};

struct Bar : Base {
    Foo foo;
};

static_assert(offsetof(Bar,foo)==0,"Error!");

Rozumiem to zachowanie całkowicie. Co ja nie rozumiem, dlaczego ta konkretna zachowanie istnieje . Został oczywiście dodany z jakiegoś powodu, ponieważ jest to wyraźny dodatek, a nie przeoczenie. Jakie jest tego uzasadnienie?

W szczególności, dlaczego dwa podstawowe podobiekty powinny mieć różne adresy? Powyżej Barjest typ i foojest zmienną składową tego typu. Nie rozumiem, dlaczego klasa podstawowa ma Barznaczenie dla klasy podstawowej typu foolub odwrotnie.

Rzeczywiście, w razie czego oczekiwałbym, że &foojest taki sam jak adres Barinstancji, która go zawiera - tak jak jest to wymagane w innych sytuacjach (1) . W końcu nie robię nic szczególnego z virtualdziedziczeniem, niezależnie od tego, klasy podstawowe są puste, a kompilacja z Base2pokazuje, że w tym konkretnym przypadku nic się nie psuje.

Ale wyraźnie to rozumowanie jest w jakiś sposób niepoprawne i istnieją inne sytuacje, w których ograniczenie to byłoby wymagane.

Powiedzmy, że odpowiedzi powinny dotyczyć C ++ 11 lub nowszej (obecnie używam C ++ 17).

(1) Uwaga: EBO został zaktualizowany w C ++ 11, a w szczególności stał się obowiązkowy dla StandardLayoutTypes (chociaż Barpowyżej nie jest a StandardLayoutType).


4
W jaki sposób uzasadnienie, które zacytowałeś („ ponieważ dwa podstawowe podobiekty tego samego typu muszą mieć różne adresy ”), nie jest wystarczające ? Różne obiekty tego samego typu muszą mieć odrębne adresy, a ten wymóg gwarantuje, że nie złamiemy tej reguły. Jeżeli pusty optymalizacji bazowa stosowana tutaj, moglibyśmy mieć Base *a = new Bar(); Base *b = a->foo;z a==b, ale ai bsą wyraźnie różne obiekty (być może z różnych metod wirtualnych nadpisania).
Toby Speight

1
Odpowiedź prawnika językowego zawiera odpowiednie części specyfikacji. I wygląda na to, że już o tym wiesz.
Deduplicator

3
Nie jestem pewien, czy rozumiem, jakiej odpowiedzi tutaj szukasz. Model obiektowy C ++ jest tym, czym jest. Ograniczenie istnieje, ponieważ wymaga tego model obiektowy. Czego więcej szukasz poza tym?
Nicol Bolas,

@TobySpeight Różne obiekty tego samego typu muszą mieć różne adresy. Można łatwo złamać tę regułę w programie o dobrze zdefiniowanym zachowaniu.
Język Lawyer

@TobySpeight Nie, nie chodzi mi o to, że zapomniałeś powiedzieć o życiu: „Różne obiekty tego samego typu z czasem życia . Możliwe jest posiadanie wielu obiektów tego samego typu, wszystkie żywe, pod tym samym adresem. Umożliwiają to co najmniej 2 błędy w sformułowaniu.
Język Lawyer

Odpowiedzi:


4

Ok, wygląda na to, że cały czas się myliłem, ponieważ dla wszystkich moich przykładów musi istnieć vtable dla obiektu podstawowego, który na początku zapobiegałby optymalizacji pustej bazy. Pozwolę, aby przykłady pozostały ważne, ponieważ uważam, że podają kilka interesujących przykładów, dlaczego unikalne adresy są zwykle dobrą rzeczą.

Po głębszym przestudiowaniu tego nie ma technicznego powodu, aby wyłączyć optymalizację pustej klasy podstawowej, gdy pierwszy element jest tego samego typu co pusta klasa podstawowa. To tylko właściwość bieżącego modelu obiektowego C ++.

Ale w C ++ 20 pojawi się nowy atrybut [[no_unique_address]]informujący kompilator, że niestatyczny element danych może nie potrzebować unikalnego adresu (technicznie rzecz biorąc, potencjalnie może on nakładać się na [intro.object] / 7 ).

Oznacza to, że (moje podkreślenie)

Niestatyczny element danych może współdzielić adres innego niestatycznego elementu danych lub adresu klasy podstawowej , [...]

stąd można „reaktywować” optymalizację pustej klasy podstawowej, nadając pierwszemu członkowi danych atrybut [[no_unique_address]]. Dodałem tutaj przykład , który pokazuje, jak działa to (i wszystkie inne przypadki, o których mogłem pomyśleć).

Błędne przykłady problemów przez to

Ponieważ wydaje się, że pusta klasa może nie mieć metod wirtualnych, dodam trzeci przykład:

int stupid_method(Base *b) {
  if( dynamic_cast<Foo*>(b) ) return 0;
  if( dynamic_cast<Bar*>(b) ) return 1;
  return 2;
}

Bar b;
stupid_method(&b);  // Would expect 0
stupid_method(&b.foo); //Would expect 1

Ale dwa ostatnie połączenia są takie same.

Stare przykłady (prawdopodobnie nie odpowiadają na pytanie, ponieważ puste klasy mogą nie zawierać metod wirtualnych)

Rozważ powyższy kod (z dodanymi wirtualnymi destruktorami) w następującym przykładzie

void delBase(Base *b) {
    delete b;
}

Bar *b = new Bar;
delBase(b); // One would expect this to be absolutely fine.
delBase(&b->foo); // Whoaa, we shouldn't delete a member variable.

Ale w jaki sposób kompilator powinien rozróżniać te dwa przypadki?

A może nieco mniej wymyślony:

struct Base { 
  virtual void hi() { std::cout << "Hello\n";}
};

struct Foo : Base {
  void hi() override { std::cout << "Guten Tag\n";}
};

struct Bar : Base {
    Foo foo;
};

Bar b;
b.hi() // Hello
b.foo.hi() // Guten Tag
Base *a = &b;
Base *z = &b.foo;
a->hi() // Hello
z->hi() // Guten Tag

Ale dwa ostatnie są takie same, jeśli mamy pustą optymalizację klasy podstawowej!


1
Można jednak argumentować, że drugie połączenie i tak ma nieokreślone zachowanie. Kompilator nie musi więc niczego rozróżniać.
StoryTeller - Unslander Monica

1
Klasa z wirtualnymi członkami nie jest pusta, więc tutaj nie ma znaczenia!
Deduplicator

1
@Deduplicator Czy masz na to standardowy cytat? Cppref mówi nam, że pusta klasa jest „klasą lub strukturą, która nie ma niestatycznych elementów danych”.
n314159

1
@ n314159 std::is_emptyna cppreference jest znacznie bardziej rozbudowany. To samo co obecny projekt na węgorzu .
Deduplicator

2
Nie możesz, dynamic_castgdy nie jest polimorficzny (z małymi wyjątkami, które nie są tu istotne).
TC
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.