Подтвердить что ты не робот

Почему может быть опасно использовать эту структуру POD как базовый класс?

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

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

  int age; 
  char type; 
};

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

4b9b3361

Ответ 1

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

Ответ 2

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

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

Ответ 3

В теории, компилятор может выставлять базовые классы по-разному. С++ 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.

Ответ 4

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

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

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

Ответ 5

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

Подробный

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

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

  • может нарушать правила С++: инструкция goto, срок службы

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

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

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

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

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