Почему вы не можете использовать offsetof для не-POD-структур в С++?

Я изучал, как получить смещение памяти члена в классе на С++ и наткнулся на это на wikipedia:

В коде С++ вы не можете использовать offsetof для доступа к элементам структур или классов, которые не являются обычными старыми структурами данных.

Я попробовал, и все работает нормально.

class Foo
{
private:
    int z;
    int func() {cout << "this is just filler" << endl; return 0;}

public: 
    int x;
    int y;
    Foo* f;

    bool returnTrue() { return false; }
};

int main()
{
    cout << offsetof(Foo, x)  << " " << offsetof(Foo, y) << " " << offsetof(Foo, f);
    return 0;
}

У меня появилось несколько предупреждений, но оно скомпилировано, и при запуске он дал разумный результат:

Laptop:test alex$ ./test
4 8 12

Я думаю, что я либо неправильно понимаю, что такое структура данных POD, либо я пропускаю еще одну часть головоломки. Я не понимаю, в чем проблема.

Ответ 1

Короткий ответ: offsetof - это функция, которая находится только в стандарте С++ для совместимости с C. Поэтому он в основном ограничен материалом, который может быть выполнен в C. С++ поддерживает только то, что он должен для совместимости C.

В качестве смещения в основном используется хак (реализованный как макрос), который опирается на простую модель памяти, поддерживающую C, от разработчиков компилятора С++ требуется большая свобода, как организовать компоновку экземпляров класса.

Эффект заключается в том, что offsetof часто работает (в зависимости от исходного кода и используемого компилятора) на С++, даже если он не поддерживается стандартом - кроме случаев, когда он отсутствует. Поэтому вы должны быть очень осторожны с использованием offsetof в С++, тем более, что я не знаю ни одного компилятора, который будет генерировать предупреждение для не-POD-использования...

Изменить. Как вы попросили, например, следующее может прояснить проблему:

#include <iostream>
using namespace std;

struct A { int a; };
struct B : public virtual A   { int b; };
struct C : public virtual A   { int c; };
struct D : public B, public C { int d; };

#define offset_d(i,f)    (long(&(i)->f) - long(i))
#define offset_s(t,f)    offset_d((t*)1000, f)

#define dyn(inst,field) {\
    cout << "Dynamic offset of " #field " in " #inst ": "; \
    cout << offset_d(&i##inst, field) << endl; }

#define stat(type,field) {\
    cout << "Static offset of " #field " in " #type ": "; \
    cout.flush(); \
    cout << offset_s(type, field) << endl; }

int main() {
    A iA; B iB; C iC; D iD;
    dyn(A, a); dyn(B, a); dyn(C, a); dyn(D, a);
    stat(A, a); stat(B, a); stat(C, a); stat(D, a);
    return 0;
}

Это приведет к сбою при попытке найти поле a внутри типа B статически, в то время как оно работает, когда экземпляр доступен. Это связано с виртуальным наследованием, где местоположение базового класса хранится в таблице поиска.

Хотя это надуманный пример, реализация может также использовать таблицу поиска, чтобы найти общедоступные, защищенные и частные разделы экземпляра класса. Или сделайте поиск полностью динамическим (используйте хеш-таблицу для полей) и т.д.

Стандарт просто оставляет все возможности открытыми, ограничивая offsetof на POD (IOW: нет возможности использовать хеш-таблицу для структур POD...:)

Еще одно замечание: мне пришлось переопределить offsetof (здесь: offset_s) для этого примера, поскольку GCC на самом деле ошибки, когда я вызываю offsetof для поля виртуального базового класса.

Ответ 2

Ответ на Bluehorn правильный, но для меня он не объясняет причину проблемы в простейших терминах. Как я понимаю, это выглядит следующим образом:

Если NonPOD не является классом POD, тогда, когда вы выполните:

NonPOD np;
np.field;

компилятор не обязательно получает доступ к полю путем добавления некоторого смещения к базовому указателю и разыменованию. Для класса POD стандарт С++ сдерживает его выполнение (или что-то подобное), но для класса, отличного от POD, это не так. Компилятор может вместо этого считывать указатель из объекта, добавлять смещение к этому значению, чтобы дать место хранения поля, а затем разыменовать. Это общий механизм с виртуальным наследованием, если поле является членом виртуальной базы NonPOD. Но это не ограничивается этим случаем. Компилятор может делать почти все, что ему нравится. Он может вызвать скрытую генерируемую компилятором функцию виртуального члена, если она захочет.

В сложных случаях, очевидно, невозможно представить местоположение поля как целочисленное смещение. Таким образом, offsetof недействителен для не-POD-классов.

В случаях, когда ваш компилятор просто так хочет хранить объект простым способом (например, однонамерное наследование и обычно даже не виртуальное множественное наследование, и обычно поля, определенные в классе, который вы ссылаетесь на объект, в отличие от какого-то базового класса), тогда это будет просто так работать. Есть, вероятно, случаи, которые просто так срабатывают для каждого отдельного компилятора. Это не делает его действительным.

Приложение: как работает виртуальное наследование?

При простом наследовании, если B является производным от A, обычная реализация заключается в том, что указатель на B является только указателем на A, а B добавлены дополнительные данные в конце:

A* ---> field of A  <--- B*
        field of A
        field of B

С простым множественным наследованием вы обычно предполагаете, что базовые классы B (вызов "em A1 и A2) расположены в некотором порядке, свойственном B. Но тот же трюк с указателями не может работать:

A1* ---> field of A1
         field of A1
A2* ---> field of A2
         field of A2

A1 и A2 "ничего не знают" о том, что они оба являются базовыми классами 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 все равно будет работать. Стандарт не требует реализаций для выполнения множественного наследования таким образом, но они часто делают (или что-то в этом роде). Таким образом, 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 должен содержать только одну копию полей 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 может быть указателем или смещением, это не имеет большого значения. В экземпляре 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 требуется больше, чем просто применить смещение. Мы должны прочитать поле "указатель на А" в В2, следовать ему и только затем применить смещение, потому что в зависимости от того, какой класс B2 является базой, этот указатель будет иметь разные значения. Нет такой вещи, как offsetof(B2,field of A): не может быть. offsetof никогда не будет работать с виртуальным наследованием, при любой реализации.

Ответ 3

В общем, когда вы спрашиваете "почему-то undefined", ответ "потому что стандарт говорит так". Обычно рациональность существует по одной или нескольким причинам, таким как:

  • трудно определить статически, в этом случае вы находитесь.

  • угловые случаи трудно определить, и никто не пострадал от определения особых случаев;

  • его использование в основном покрывается другими функциями;

  • существующие практики во время стандартизации были разными, а нарушение существующей реализации и программ в зависимости от них было сочтено более вредным для стандартизации.

Возвращаясь к офсету, вторая причина, вероятно, является доминирующей. Если вы посмотрите на С++ 0X, где стандарт ранее использовал POD, теперь он использует "стандартную компоновку", "совместимость с макетами", "POD", позволяющую более совершенные случаи. И теперь для офсетных классов требуются классы "стандартного макета", которые являются случаями, когда комитет не хотел форматировать макет.

Вы также должны рассмотреть возможность использования offsetof(), которое должно получить значение поля, если у вас есть указатель void *. Множественное наследование - виртуальное или нет - проблематично для этого использования.

Ответ 4

Для определения структуры данных POD здесь вы найдете объяснение [уже отправленное в другое сообщение в переполнении стека]

Что такое типы POD в С++?

Теперь, придя к вашему коду, он работает нормально, как ожидалось. Это происходит потому, что вы пытаетесь найти offsetof() для общедоступных членов вашего класса, который действителен.

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

Ответ 5

Я думаю, что ваш класс соответствует определению С++ 0x POD. g++ реализовал некоторые из С++ 0x в своих последних выпусках. Я думаю, что VS2008 также имеет в нем несколько С++ 0x бит.

Из статья wikipedia С++ 0x

С++ 0x смягчит несколько правил в отношении определения POD.

Класс/struct считается POD, если он тривиален, стандартно-макет и если все его нестатические члены Стручки.

Определен тривиальный класс или структура как:

  • Имеет тривиальный конструктор по умолчанию. Это может использовать значение по умолчанию синтаксис конструктора (SomeConstructor() = default;).
  • Имеет тривиальный конструктор копирования, который может использовать синтаксис по умолчанию.
  • Имеет тривиальный оператор присваивания копии, который может использовать значение по умолчанию синтаксис.
  • Имеет тривиальный деструктор, который не должен быть виртуальным.

Класс или структура стандартного макета определяемый как:

  • Имеет только нестатические элементы данных, имеющие тип стандартного макета
  • Имеет тот же контроль доступа (открытый, закрытый, защищенный) для всех нестатические элементы
  • Не имеет виртуальных функций
  • Не имеет виртуальных базовых классов
  • Имеет только базовые классы со стандартным макетом
  • Не имеет базовых классов того же типа, что и первый нестатический Член
  • Либо не имеет базовых классов с нестатическими членами, либо не имеет нестатические члены данных в большинстве производный класс и не более одной базы класс с нестационарными членами. В сущность, может быть только один класс в иерархии этого класса, которая имеет нестатические элементы.

Ответ 6

Если вы добавите, например, виртуальный пустой деструктор:

virtual ~Foo() {}

Ваш класс станет "полиморфным", т.е. будет иметь скрытое поле элемента, которое является указателем на "vtable", который содержит указатели на виртуальные функции.

Из-за скрытого поля элемента размер объекта и смещение членов не будут тривиальными. Таким образом, вы должны получить проблемы с использованием offsetof.

Ответ 7

Кажется, это отлично работает для меня:

#define myOffset(Class,Member) ({Class o; (size_t)&(o.Member) - (size_t)&o;})

Ответ 8

Работает для меня

   #define get_offset(type, member) ((size_t)(&((type*)(1))->member)-1)
   #define get_container(ptr, type, member) ((type *)((char *)(ptr) - get_offset(type, member)))

Ответ 9

В С++ вы можете получить относительное смещение следующим образом:

class A {
public:
  int i;
};

class B : public A {
public:
  int i;
};

void test()
{
  printf("%p, %p\n", &A::i, &B::i); // edit: changed %x to %p
}

Ответ 10

Бьюсь об заклад, вы скомпилируете это с помощью VС++. Теперь попробуйте с g++ и посмотрите, как это работает...

Короче говоря, это undefined, но некоторые компиляторы могут это разрешить. Другие нет. В любом случае он не переносится.