Почему может быть опасно использовать эту структуру POD в качестве базового класса? - PullRequest
23 голосов
/ 19 августа 2011

У меня был этот разговор с коллегой, и он оказался интересным. Скажем, у нас есть следующий класс POD

struct A { 
  void clear() { memset(this, 0, sizeof(A)); } 

  int age; 
  char type; 
};

clear предназначено для очистки всех элементов с настройкой 0 (в байтовом выражении). Что может пойти не так, если мы используем A в качестве базового класса? Здесь есть тонкий источник ошибок.

Ответы [ 5 ]

17 голосов
/ 19 августа 2011

Компилятор может добавить байты заполнения к A. Таким образом, sizeof(A) выходит за пределы char type (до конца заполнения).Однако в случае наследования компилятор может не добавить заполненные байты.Таким образом, вызов memset перезапишет часть подкласса.

4 голосов
/ 19 августа 2011

В дополнение к другим примечаниям sizeof является оператором времени компиляции, поэтому clear() не будет обнулять элементы, добавленные производными классами (за исключением отмеченных из-за странности заполнения).

В этом нет ничего действительно "тонкого";memset ужасно использовать в C ++.В тех редких случаях, когда вы действительно можете просто заполнить память нулями и ожидать нормального поведения, и вам действительно нужно заполнить память нулями, и , инициализируя все нулями через список инициализаторов.цивилизованный путь как-то неприемлем, используйте вместо него std::fill.

2 голосов
/ 19 августа 2011

Теоретически, компилятор может размещать базовые классы по-разному. C ++ 03 §10 параграф 5 гласит:

Подобъект базового класса может иметь компоновку (3.7), отличную от компоновки наиболее производного объекта того же типа.

Как отмечал StackedCrooked , это может произойти, если компилятор добавляет заполнение в конец базового класса A, когда он существует как собственный объект, но компилятор может не добавить это заполнение, когда Базовый класс. Это заставит A::clear() перезаписать первые несколько байтов членов подкласса.

Однако на практике мне не удалось этого добиться ни с GCC, ни с Visual Studio 2008. Использование этого теста:

struct A
{
  void clear() { memset(this, 0, sizeof(A)); }

  int age;
  char type;
};

struct B : public A
{
  char x;
};

int main(void)
{
  B b;
  printf("%d %d %d\n", sizeof(A), sizeof(B), ((char*)&b.x - (char*)&b));
  b.x = 3;
  b.clear();
  printf("%d\n", b.x);

  return 0;
}

И изменение A, B или обоих для «упаковки» (с #pragma pack в VS и __attribute__((packed)) в GCC), я не мог ' b.x может быть перезаписано в любом случае. Оптимизации были включены. 3 значения, напечатанные для размеров / смещений, всегда были 8/12/8, 8/9/8 или 5/6/5.

0 голосов
/ 28 июля 2015

Коротко : Мне кажется, что единственная потенциальная проблема заключается в том, что я не могу найти никакой информации о гарантиях "заполнения байтов" в стандартах C89, C2003 ... Есть ли у них какие-то экстраординарныеизменчивое или только для чтения поведение - я не могу найти даже то, что термин "байты заполнения" подразумевает под стандартами ...

Подробно :

Для объектов типов POD этоСтандарт C ++ 2003 гарантирует, что:

  • , когда вы запоминаете содержимое вашего объекта в массив char или unsigned char, а затем запоминаете содержимое обратно в ваш объект, объект будетсохраните свое первоначальное значение
  • гарантировано, что в начале объекта POD не будет заполнения

  • может нарушить правила C ++ относительно: оператора goto, времени жизни

Для C89 также существуют некоторые гарантии относительно структур:

  • При использовании для смеси объединяющих структур, если структуры имеют одинаковое начало, то сначалакомпоЭлементы имеют идеальное отображение

  • Размер структур в C равен объему памяти для хранения всех компонентов, расположению под заполнением между компонентами, размещению заполнению под следующими структурами

  • В C компонентам структуры даны адреса.Существует гарантия того, что компоненты адреса находятся в порядке возрастания.И адрес первого компонента совпадает с начальным адресом структуры.Независимо от того, на каком компьютере выполняется программа,

Так что мне кажется, что такие правила подходят и для C ++, и все в порядке.Я действительно думаю, что на аппаратном уровне никто не будет ограничивать вас от записи в байтах заполнения для неконстантного объекта.

0 голосов
/ 19 августа 2011

Метод clear базового класса устанавливает только значения членов класса.

Согласно правилам выравнивания, компилятору разрешено вставлять заполнение, чтобы следующий элемент данных находился на выровненной границе. Таким образом, после элемента данных type будет заполнение. Первый элемент данных потомка займет этот слот и будет свободен от эффектов memset, поскольку базовый класс sizeof не включает размер потомка. Размер родителя! = Размер дочернего элемента (если только у дочернего элемента нет данных). См. Нарезка .

Упаковка структур не является частью языкового стандарта. Надеемся, что при хорошем компиляторе размер упакованной структуры не включает никаких дополнительных байтов после последнего. Даже в этом случае упакованный потомок, унаследованный от упакованного родителя, должен давать тот же результат: родитель устанавливает только элементы данных в родительском объекте.

...