Почему может быть опасно использовать эту структуру POD как базовый класс?
У меня был этот разговор с коллегой, и это оказалось интересным. Скажем, у нас есть следующий класс POD
struct A {
void clear() { memset(this, 0, sizeof(A)); }
int age;
char type;
};
clear
предназначен для очистки всех участников, установка на 0
(байт). Что может пойти не так, если мы используем A
как базовый класс? Здесь есть тонкий источник ошибок.
Ответы
Ответ 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 компьютер, на котором выполняется программа
Итак, мне кажется, что такие правила подходят и для С++, и все в порядке. Я действительно думаю, что на аппаратном уровне никто не будет ограничивать вас от записи в байтах заполнения для неконстантного объекта.