Почему вы не можете использовать offsetof на не-POD структурах в C ++? - PullRequest
49 голосов
/ 15 июля 2009

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

В коде C ++ нельзя использовать 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, либо мне не хватает какой-то другой части головоломки. Я не вижу, в чем проблема.

Ответы [ 11 ]

42 голосов
/ 15 июля 2009

Ответ Блюхорна верен, но для меня он не объясняет причину проблемы в самых простых выражениях. Как я понимаю, это так:

Если 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 будет никогда работать с виртуальным наследованием, в любой реализации.

35 голосов
/ 15 июля 2009

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

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

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

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

#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 для поля виртуального базового класса.

6 голосов
/ 15 июля 2009

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

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

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

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

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

Возвращаясь к offsetof, вторая причина, вероятно, является доминирующей. Если вы посмотрите на C ++ 0X, где стандарт ранее использовал POD, то теперь он использует «стандартную компоновку», «совместимость с компоновкой», «POD», что позволяет более детализировать случаи. А для offsetof теперь нужны классы «стандартного макета», в тех случаях, когда комитет не хотел форсировать макет.

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

2 голосов
/ 15 июля 2009

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

Из статьи Википедии c ++ 0x

C ++ 0x ослабит несколько правил в отношении определения POD.

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

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

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

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

  1. Имеет только нестатические элементы данных, которые имеют тип стандартной компоновки
  2. Имеет одинаковый контроль доступа (публичный, частный, защищенный) для всех нестатические элементы
  3. Не имеет виртуальных функций
  4. Не имеет виртуальных базовых классов
  5. Имеет только базовые классы стандартного типа
  6. Не имеет базовых классов того же типа, что и первый определенный нестатический член
  7. Либо не имеет базовых классов с нестатическими членами, либо не имеет Нестатические члены данных в большинстве производный класс и не более одной базы класс с нестатическими членами. В сущность, может быть только один класс в иерархии этого класса, которая имеет нестатические элементы.
1 голос
/ 15 июля 2009

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

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

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

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

0 голосов
/ 16 августа 2018

Это работает каждый раз, и это самая переносимая версия для использования в c и c ++

#define offset_start(s) s
#define offset_end(e) e
#define relative_offset(obj, start, end) ((int64_t)&obj->offset_end(end)-(int64_t)&obj->offset_start(start))

struct Test {
     int a;
     double b;
     Test* c;
     long d;
 }


int main() {
    Test t;
    cout << "a " << relative_offset((&t), a, a) << endl;
    cout << "b " << relative_offset((&t), a, b) << endl;
    cout << "c " << relative_offset((&t), a, c) << endl;
    cout << "d " << relative_offset((&t), a, d) << endl;
    return 0;
}

Приведенный выше код просто требует, чтобы вы содержали экземпляр какого-либо объекта, будь то структура или класс. Затем вам нужно передать указатель на класс или структуру, чтобы получить доступ к его полям. Чтобы убедиться, что вы получили правильное смещение, никогда не устанавливайте поле «начало» под полем «конец». Мы используем компилятор, чтобы выяснить, каково смещение адреса во время выполнения.

Это позволяет вам не беспокоиться о проблемах с данными заполнения компилятора и т. Д.

0 голосов
/ 17 декабря 2013

у меня работает

   #define get_offset(type, member) ((size_t)(&((type*)(1))->member)-1)
   #define get_container(ptr, type, member) ((type *)((char *)(ptr) - get_offset(type, member)))
0 голосов
/ 29 апреля 2013

Мне кажется, это нормально работает:

#define myOffset(Class,Member) ({Class o; (size_t)&(o.Member) - (size_t)&o;})
0 голосов
/ 15 июля 2009

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

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

0 голосов
/ 15 июля 2009

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

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
}
...