Що запобігає перекриттю сусідніх членів у класах?


12

Розглянемо наступні три structs:

class blub {
    int i;
    char c;

    blub(const blub&) {}
};

class blob {
    char s;

    blob(const blob&) {}
};

struct bla {
    blub b0;
    blob b1;
};

На типових платформах, де intрозміщено 4 байти, розміри, вирівнювання та загальна заливка 1 є такими:

  struct   size   alignment   padding  
 -------- ------ ----------- --------- 
  blub        8           4         3  
  blob        1           1         0  
  bla        12           4         6  

Між сховищами blubта blobелементами немає перекриття , хоча розмір 1 blobв принципі міг би "поміститися" в набивання blub.

C ++ 20 вводить no_unique_addressатрибут, який дозволяє сусіднім порожнім членам ділити ту саму адресу. Це також явно дозволяє описаний вище сценарій використання прокладки одного члена для зберігання іншого. З cppreference (моє наголос):

Вказує, що цей член даних не повинен мати адресу, відмінну від усіх інших нестатичних членів даних свого класу. Це означає, що якщо член має порожній тип (наприклад, Allocator без стану), компілятор може оптимізувати його, щоб він не займав місця, як, якби це була порожня база. Якщо член не порожній, будь-які хвостові накладки в ньому також можуть бути повторно використані для зберігання інших членів даних.

Дійсно, якщо ми будемо використовувати цей атрибут на blub b0, розмір blaкрапель до 8, тож blobсправді зберігається в тому blub , що видно на Godbolt .

Нарешті, ми переходимо до мого питання:

Який текст у стандартах (C ++ 11 до C ++ 20) перешкоджає цьому накладенню без no_unique_addressоб’єктів, які не можна тривіально скопіювати?

Мені потрібно виключити тривіально копіювані (ТС) об’єкти із вищезазначеного, тому що для об'єктів ТС дозволено переходити std::memcpyвід одного об’єкта до іншого, включаючи суб’єкти членів, і якщо сховище буде перекрито, це порушиться (тому що все або частина сховища для сусіднього члена буде переписано) 2 .


1 Ми обчислюємо прокладку просто як різницю між розмірами конструкції та розміром усіх її складових, рекурсивно.

2 Тому у мене визначені конструктори копій: робити, blubа blobне тривіально копіювати .


Я цього не досліджував, але здогадуюсь "як би" правила. Якщо немає помітної різниці (терміна з дуже конкретним значенням btw) до абстрактної машини (з чого складається ваш код), тоді компілятор може змінити код, як би він не любив.
Jesper Juhl

Упевнений , що це боян цього: stackoverflow.com/questions/53837373 / ...
NathanOliver

@JesperJuhl - правильно, але я запитую, чому це не можна , чому не можна , і правило "як би" зазвичай стосується першого, але не має сенсу для останнього. Крім того, "як би" не зрозуміло для структури структури, яка зазвичай є глобальною проблемою, а не локальною. Зрештою, компілятор повинен мати єдиний послідовний набір правил для компонування, за винятком можливо структур, які можуть виявитись ніколи не «втеченими».
BeeOnRope

1
@BeeOnRope Не можу відповісти на ваше запитання, вибачте. Тому я просто опублікував коментар, а не відповідь. Те, що ви отримали в цьому коментарі, було моєю найкращою здогадкою щодо пояснення, але я не знаю відповіді (цікаво, щоб дізнатися це сам - саме тому ви отримали нагороду).
Jesper Juhl

1
@NicolBolas - ти відповідаєш на правильне запитання? Йдеться не про виявлення безпечних копій чи чогось іншого. Швидше мені цікаво, чому прокладки не можна використовувати повторно між членами. У будь-якому випадку, ви не праві: тривіальна копійованим є властивістю типу і завжди. Тим НЕ менше, для безпечного копіювання об'єкта він повинен як мати тип ТЗ (властивість типу), а не бути потенційно перекриваються об'єкта зйомки (властивість об'єкта, який я припускаю, де ви заплуталися). Досі не знаю, чому ми тут говоримо про копії.
BeeOnRope

Відповіді:


1

Цей стандарт жахливо тихий, якщо говорити про модель пам'яті, і не дуже чітко про деякі терміни, які вона використовує. Але я думаю, що я знайшов робочу аргументацію (це може бути трохи слабко)

Спочатку давайте з’ясуємо, що є навіть частиною об’єкта. [основні типи] / 4 :

Об'єктне представлення об'єкта типу T- це послідовність N unsigned charоб'єктів, зайнята об'єктом типу T, де Nдорівнює sizeof(T). Представлення значення об'єкта типу T- це набір бітів, які беруть участь у представленні значення типу T. Біти в об'єктному поданні, які не є частиною представлення значення, є бітами прокладки.

Отже, представлення об'єкта b0складається з sizeof(blub) unsigned charоб'єктів, тобто 8 байт. Набивні шматочки є частиною об'єкта.

Жоден об'єкт не може займати простір іншого, якщо він не є вкладеним всередині нього [basic.life] /1.5 :

Термін експлуатації об'єкта oтипу Tзакінчується, коли:

[...]

(1.5) сховище, яке займає об'єкт, вивільняється або повторно використовується об'єктом, який не вкладений всередину o([вступ.об'єкт]).

Так b0закінчився би термін експлуатації , коли сховище, яке займає його, буде повторно використане іншим об'єктом, тобто b1. Я цього не перевіряв, але вважаю, що стандартний мандат на те, щоб субектив об'єкта, що живе, також повинен бути живим (і я не міг уявити, як це має працювати інакше).

Тому сховище, яке b0 займає, може не використовувати b1. Я не знайшов визначення "займати" у стандарті, але думаю, що розумне тлумачення було б "частиною представлення об'єкта". У цитаті, що описує представлення об'єкта, вживаються слова "взяти на себе" 1 . Тут це було б 8 байт, тому blaпотрібно хоча б ще один b1.

Спеціально для суб'єктів (тому серед інших нестатичних членів даних) також існує умова [intro.object] / 9 (але це було додано із C ++ 20, thx @BeeOnRope)

Два об’єкти із життям, що перекриваються, що не є бітовими полями, можуть мати однакову адресу, якщо один вкладений в інший, або якщо принаймні один є суб'єктом нульового розміру і вони різного типу; в іншому випадку вони мають окремі адреси і займають непересічні байти зберігання .

(наголос мій) Тут знову є проблема, яка "займає" не визначена, і я знову заперечую, щоб взяти байти в зображенні об'єкта. Зауважте, що до цього [basic.memobj] / виноски 29 є виноска

За правилом "як-якщо" реалізація дозволена зберігати два об'єкти на одній машинній адресі або взагалі не зберігати об'єкт, якщо програма не може помітити різницю ([intro.execution]).

Що може дозволити компілятору зламати це, якщо він зможе довести відсутність помітних побічних ефектів. Я думаю, що це досить складно для такої фундаментальної речі, як компонування об'єкта. Можливо, саме тому ця оптимізація приймається лише тоді, коли користувач надає інформацію про те, що немає ніяких причин мати нерозбірливі об’єкти, додаючи [no_unique_address]атрибут.

tl; dr: Прокладка, можливо, частина предмета, і члени повинні бути роз'єднані.


1 Я не міг протистояти доданню посилання, яке займає, може означати, щоб взяти участь: Вебстерський переглянений невпорядкований словник, G. & C. Merriam, 1913 р. (Наголос мій)

  1. Розмістити або заповнити розміри; зайняти приміщення або простір; покривати чи заповнювати; так, табір займає п'ять гектарів землі. Сер Дж. Гершель.

Яке стандартне сканування було б повним без сканування словника?


2
Частини "зайняти нерозбірливі байти зберігання" з into.storage для мене вистачило б - але це формулювання було додано лише в C ++ 20 як частина зміни, яка додалася no_unique_address. Це залишає ситуацію до C ++ 20 менш зрозумілою. Я не розумів ваших міркувань, що призводять до того, що "жоден об'єкт не може займати простір іншого, якщо він не вкладений всередині нього" з basic.life/1.5, зокрема, як дістатися з "звільняється сховище, яке займає об'єкт" щоб "жоден об'єкт не може займати простір іншого".
BeeOnRope

1
До цього пункту я додав невелике уточнення. Я сподіваюся, що це зробить більш зрозумілим. В іншому випадку я завтра перегляну це ще раз, зараз мені досить пізно.
n314159

"Два об'єкти з життями, що перекриваються, які не є бітовими полями, можуть мати однакову адресу, якщо один вкладений в інший, або якщо принаймні один - це субоб'єкт нульового розміру, і вони мають різні типи" 2 об'єкти із життям, що перекриваються, з одного типу, мають однакову адресу .
юрист з мови

Вибачте, ви могли б детальніше? Ви цитуєте стандартну цитату з моєї відповіді і наводите приклад, який трохи суперечить цьому. Я не впевнений, чи це коментар до моєї відповіді, і якщо саме він повинен мені сказати. Що стосується вашого прикладу, я б сказав, що потрібно було враховувати ще інші частини стандарту (є абзац про безпідписаний масив знаків, що забезпечує зберігання для іншого об'єкта, щось щодо оптимізації бази за нульовим розміром, і все ж далі слід також шукати, якщо розміщення нове має спеціальні надбавки, всі речі, які, на мою думку, не стосуються прикладу ОП)
n314159,

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