Почему оптимизация пустой базы запрещена, если пустой базовый класс также является переменной-членом? - PullRequest
14 голосов
/ 08 января 2020

Оптимизация пустой базы отлично. Тем не менее, он имеет следующее ограничение:

Оптимизация пустой базы запрещена, если один из пустых базовых классов также является типом или базой для типа первых неустановленных c данных. член, так как два базовых подобъекта одного и того же типа должны иметь разные адреса в представлении объекта самого производного типа.

Чтобы объяснить это ограничение, рассмотрим следующий код. static_assert не удастся. Принимая во внимание, что изменение Foo или Bar вместо наследования от Base2 предотвратит ошибку:

#include <cstddef>

struct Base  {};
struct Base2 {};

struct Foo : Base {};

struct Bar : Base {
    Foo foo;
};

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

Я полностью понимаю это поведение. Я не понимаю, что почему это конкретное поведение существует . Это было очевидно добавлено по причине, так как это явное дополнение, а не недосмотр. Чем это обосновано?

В частности, почему два базовых подобъекта должны иметь разные адреса? Выше Bar - это тип, а foo - переменная-член этого типа. Я не понимаю, почему базовый класс Bar имеет значение для базового класса типа foo или наоборот.

Действительно, я, если что-нибудь, ожидал бы, что &foo совпадает с адресом экземпляра Bar, в котором он находится, - это требуется в других ситуациях (1) . В конце концов, я не делаю ничего необычного с virtual наследованием, базовые классы пусты независимо, а компиляция с Base2 показывает, что в этом конкретном случае ничего не сломалось.

Но ясно, что это рассуждение как-то неверно, и есть другие ситуации, когда это ограничение потребуется.

Допустим, ответы должны быть для C ++ 11 или новее (в настоящее время я использую C ++ 17).

(1) Примечание: EBO был обновлен в C ++ 11 и, в частности, стал обязательным для StandardLayoutType s (хотя Bar, выше, не является StandardLayoutType).

1 Ответ

4 голосов
/ 08 января 2020

Хорошо, кажется, что я все время ошибался, так как для всех моих примеров должна существовать vtable для базового объекта, который не позволял бы начинать пустую базовую оптимизацию. Я оставлю примеры в силе, так как я думаю, что они дают некоторые интересные примеры того, почему уникальные адреса, как правило, полезно иметь.

Изучив все это более подробно, нет технической причины для пустой оптимизации базового класса. быть отключенным, когда первый член того же типа, что и пустой базовый класс. Это просто свойство текущей объектной модели C ++.

Но в C ++ 20 появится новый атрибут [[no_unique_address]], который сообщает компилятору о том, что элементу данных, не находящемуся в состоянии c, может не потребоваться уникальный адрес (технически говоря, это , потенциально перекрывающийся) [intro.object] / 7 ).

Это означает, что (выделение мое)

Элемент данных, не являющийся объектом c, может совместно использовать адрес другого элемента данных, не являющегося объектом c, или , который базового класса , [...]

, следовательно, можно «реактивировать» пустую оптимизацию базового класса, дав первому члену данных атрибут [[no_unique_address]]. Я добавил пример здесь , который показывает, как это (и все другие случаи, которые я мог придумать) работает.

Неправильные примеры проблем из-за этого

Поскольку кажется, что пустой класс может не иметь виртуальных методов, позвольте мне добавить третий пример:

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

Но последние два вызова одинаковы.

Старые примеры (Вероятно, не отвечайте на вопрос, поскольку пустые классы могут не содержать виртуальные методы, кажется)

Рассмотрите в своем коде выше (с добавленными виртуальными деструкторами) следующий пример

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.

Но как должен компилятор различать guish эти два случая?

И, может быть, чуть менее надуманно:

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

Но последние два одинаковы, если мы имеем Оптимизация пустого базового класса!

...