Чому оптимізація порожньої бази заборонена, коли порожній базовий клас також є змінною члена?


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).


4
Як пояснюється цитуване вами обґрунтування (" оскільки два базових суб'єкти одного типу мають мати різні адреси ") не виходить ? Різні об'єкти одного типу повинні мати різні адреси, і ця вимога гарантує, що ми не порушуємо це правило. Якщо порожня оптимізація бази застосовується тут, ми могли б Base *a = new Bar(); Base *b = a->foo;з a==b, але aі bявно різні об'єкти (можливо , з різними переопределяет віртуальний метод).
Toby Speight

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

Але останні два такі самі, якщо у нас порожня оптимізація базового класу!


1
Можна стверджувати, що другий виклик все-таки має невизначену поведінку. Тож компілятору нічого не потрібно відрізняти.
StoryTeller - Невідповідна Моніка

1
Клас з будь-якими віртуальними членами не порожній, тому тут неважливо!
Дедуплікатор

1
@Deduplicator У вас є стандартна цитата на це? Cppref повідомляє нам, що порожній клас - це "клас або структура, яка не має нестатичних членів даних".
n314159

1
@ n314159 std::is_emptyщодо cppreference набагато складніший. Те ж з поточного проекту на eel.is .
Дедуплікатор

2
Ви не можете, dynamic_castколи це не поліморфно (за незначними винятками тут не доречно).
ТК
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.