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

14

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

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

Чтобы объяснить это ограничение, рассмотрим следующий код. Не 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 и, в частности, стал обязательным для StandardLayoutTypes (хотя Bar, выше, это не a StandardLayoutType).

imallett
источник
4
Как обоснование, которое вы процитировали (« поскольку два базовых подобъекта одного типа должны иметь разные адреса »), не дало результатов? Разные объекты одного типа должны иметь разные адреса, и это требование гарантирует, что мы не нарушаем это правило. Если пустая оптимизация базы применяется здесь, мы могли бы Base *a = new Bar(); Base *b = a->foo;с a==b, но aи bявно разные объекты (возможно , с разными переопределяет виртуальный метод).
Тоби Спейт
1
Язык-адвокат ответ цитирует соответствующие части спецификации. И, кажется, вы уже знаете об этом.
дедупликатор
3
Я не уверен, что понимаю, какой ответ вы ищете здесь. Объектная модель C ++ - это то, что она есть. Ограничение есть, потому что этого требует объектная модель. Что еще вы ищете помимо этого?
Никол Болас
@TobySpeight Различные объекты одного типа должны иметь разные адреса. Это правило легко нарушить в программе с четко определенным поведением.
Языковой адвокат
@ TobySpeight Нет, я не имею в виду, что вы забыли сказать о времени жизни: «Различные объекты одного типа в течение их жизни » . Можно иметь несколько объектов одного типа, все живые, по одному и тому же адресу. В формулировке есть как минимум 2 ошибки, позволяющие это сделать.
Языковой адвокат

Ответы:

4

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

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

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

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

Нестатический член данных может совместно использовать адрес другого не статического члена данных или адрес базового класса , [...]

следовательно, можно «реактивировать» пустую оптимизацию базового класса, дав первому члену данных атрибут [[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.

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

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

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

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

n314159
источник
1
Можно, однако, утверждать, что второй вызов имеет неопределенное поведение в любом случае. Таким образом, компилятор не должен ничего различать.
StoryTeller - Unslander Моника
1
Класс с любыми виртуальными членами не пустой, поэтому здесь неактуально!
дедупликатор
1
@Deduplicator У вас есть стандартная цитата на это? Cppref говорит нам, что пустой класс - это «класс или структура, которые не имеют нестатических членов-данных».
n314159
1
@ n314159 std::is_emptyна cppreference гораздо сложнее. То же из текущего проекта на eel.is .
дедупликатор
2
Вы не можете, dynamic_castкогда это не полиморфно (с незначительными исключениями, не относящимися к делу).
TC