Ответ Блюхорна верен, но для меня он не объясняет причину проблемы в самых простых выражениях. Как я понимаю, это так:
Если NonPOD - это класс не POD, то когда вы делаете:
NonPOD np;
np.field;
компилятор не обязательно обращается к полю путем добавления некоторого смещения к базовому указателю и разыменования. Для класса POD Стандарт C ++ заставляет его делать это (или что-то эквивалентное), но для класса без POD это не так. Вместо этого компилятор может прочитать указатель из объекта, добавить смещение к этому значению , чтобы указать место хранения поля, а затем разыменовать. Это общий механизм с виртуальным наследованием, если поле является членом виртуальной базы NonPOD. Но это не ограничивается этим случаем. Компилятор может делать практически все что угодно. При желании он может вызвать скрытую виртуальную функцию-член, созданную компилятором.
В сложных случаях, очевидно, невозможно представить местоположение поля как целочисленное смещение. Так что offsetof
недопустимо в классах, отличных от POD.
В тех случаях, когда ваш компилятор просто хранит объект простым способом (например, одиночное наследование и, как правило, даже не виртуальное множественное наследование, и обычно поля определяются прямо в классе, на который вы ссылаетесь, в отличие от какого-то базового класса), тогда так и будет работать. Вероятно, есть случаи, которые просто работают на каждом существующем компиляторе. Это не делает его действительным.
Приложение: как работает виртуальное наследование?
При простом наследовании, если B получен из A, обычная реализация состоит в том, что указатель на B является просто указателем на A, с дополнительными данными B, застрявшими в конце:
A* ---> field of A <--- B*
field of A
field of B
При простом множественном наследовании вы обычно предполагаете, что базовые классы B (назовите их A1 и A2) расположены в некотором порядке, свойственном B. Но тот же прием с указателями не может работать:
A1* ---> field of A1
field of A1
A2* ---> field of A2
field of A2
А1 и А2 «ничего не знают» о том факте, что они оба являются базовыми классами B. Поэтому, если вы разыгрываете B * на A1 *, он должен указывать на поля A1, и если вы разыгрываете его на A2 * он должен указывать на поля A2. Оператор преобразования указателя применяет смещение. Таким образом, вы можете получить следующее:
A1* ---> field of A1 <---- B*
field of A1
A2* ---> field of A2
field of A2
field of B
field of B
Затем приведение B * к A1 * не изменяет значение указателя, но приведение к A2 * добавляет sizeof(A1)
байт. Это «другая» причина, по которой при отсутствии виртуального деструктора удаление B через указатель на A2 происходит неправильно. Он не просто не может вызвать деструктор B и A1, он даже не освобождает правильный адрес.
В любом случае, B "знает", где находятся все его базовые классы, они всегда хранятся с одинаковыми смещениями. Так что в этой договоренности смещение все равно будет работать. Стандарт не требует, чтобы реализации делали множественное наследование таким способом, но они часто делают (или что-то подобное). Таким образом, offsetof может работать в этом случае на вашей реализации, но это не гарантируется.
А как насчет виртуального наследования? Предположим, что B1 и B2 имеют A в качестве виртуальной базы. Это делает их классами с одним наследованием, поэтому вы можете подумать, что первый трюк снова будет работать:
A* ---> field of A <--- B1* A* ---> field of A <--- B2*
field of A field of A
field of B1 field of B2
Но держись. Что происходит, когда C выводится (не виртуально, для простоты) как из B1, так и из B2? C должен содержать только 1 копию полей A. Эти поля не могут непосредственно предшествовать полям B1, а также непосредственно предшествовать полям B2. У нас проблемы.
Итак, что могут сделать реализации:
// an instance of B1 looks like this, and B2 similar
A* ---> field of A
field of A
B1* ---> pointer to A
field of B1
Хотя я указал B1 *, указывающий на первую часть объекта после подобъекта A, я подозреваю (не удосужившись проверить), что фактического адреса там не будет, это будет начало A. просто в отличие от простого наследования смещения между фактическим адресом в указателе и адресом, который я указал на диаграмме, будут никогда использоваться, если только компилятор не уверен в динамическом типе объекта. Вместо этого он всегда будет проходить через мета-информацию, чтобы правильно добраться до A. Таким образом, мои диаграммы будут указывать там, так как это смещение всегда будет применяться для целей, которые нас интересуют.
"Указатель" на A может быть указателем или смещением, это не имеет значения. В экземпляре B1, созданном как B1, он указывает на (char*)this - sizeof(A)
, и то же самое в экземпляре B2. Но если мы создадим C, он может выглядеть так:
A* ---> field of A
field of A
B1* ---> pointer to A // points to (char*)(this) - sizeof(A) as before
field of B1
B2* ---> pointer to A // points to (char*)(this) - sizeof(A) - sizeof(B1)
field of B2
C* ----> pointer to A // points to (char*)(this) - sizeof(A) - sizeof(B1) - sizeof(B2)
field of C
field of C
Таким образом, для доступа к полю A с помощью указателя или ссылки на B2 требуется нечто большее, чем просто применение смещения. Мы должны прочитать поле «указатель на A» в B2, следовать ему и только затем применять смещение, потому что в зависимости от того, к какому классу B2 относится база, этот указатель будет иметь разные значения. Нет такой вещи как offsetof(B2,field of A)
: не может быть. offsetof будет никогда работать с виртуальным наследованием, в любой реализации.