Ищете что-то похожее на offsetof () для типов, отличных от POD

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

Вот почему:

Я хочу хранить данные в формате HDF5, который кажется наиболее подходящим для моего типа материал (вывод численного моделирования), но, возможно, это библиотека, скорее ориентированная на C. Я хочу использовать его через интерфейс C ++, для чего мне потребуется объявить такие типы хранилищ (следующая документация от 1CompType.html # a16 "rel =" noreferrer "> здесь и здесь (раздел 4.3.2.1.1)):

class example { 
public:
    double member_a;
    int member_b;
} //class example

H5::CompType func_that_creates_example_CompType() {
    H5::CompType ct;
    ct.insertMember("a", HOFFSET(example, member_a), H5::PredType::NATIVE_DOUBLE);
    ct.insertMember("b", HOFFSET(example, member_b), H5::PredType::NATIVE_INT);
    return ct;
} //func_that_creates_example_CompType

где HOFFSET - это специфичный для HDF макрос, который использует offsetof.

Проблема, конечно, в том, что как только класс-пример становится немного более функциональным, он больше не относится к типу POD, и поэтому использование offsetof даст неопределенные результаты.

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

Так что я надеялся, что у вас появятся идеи получше. В идеале я бы искал переносимое решение этой проблемы, но если этого не произойдет, вы могли бы дать мне идею, которая работает на x86 и x86_64 с GCC, я уже был бы безмерно благодарен.

----- добавлено позже: -----

Грег Хьюгилл предложил ниже хранить данные в простой структуре, а затем построить фактический класс, унаследовав от него. В частности, для HDF, я думаю, это может практически не сработать. Более сложный сценарий использования, чем указано выше:

class base_pod {
public:
    double member_a;
    int member_b;
}; //class base_pod

class derived_non_pod : private base_pod {
public:
    //the following method is only virtual to illustrate the problem
    virtual double get_member_a() {return member_a; }
}; //class derived_non_pod

class that_uses_derived_non_pod {
public:
    void whatever();
private:
    derived_non_pod member_c;
}; //class that_uses_derived_non_pod

Теперь, когда мы храним экземпляры класса that_uses_dehibited_non_pod, мы не можем описать его структуру памяти, как если бы у него был base_pod как member_c. Это приведет к неправильному смещению, потому что производное_non_pod добавляет забавные вещи (вроде таблицы виртуальных функций, я думаю?).


person yungchin    schedule 07.10.2008    source источник


Ответы (7)


Решение Грега Хьюгилла, вероятно, предпочтительнее этого (возможно, с композицией, а не наследованием).

Однако я думаю, что с GCC на x86 и x86_64 offsetof будет работать даже для членов типов, отличных от POD, если это «имеет смысл». Так, например, это не будет работать для членов, унаследованных от виртуальных базовых классов, потому что в GCC это реализовано с дополнительным косвенным обращением. Но пока вы придерживаетесь простого общедоступного одиночного наследования, GCC просто размещает ваши объекты таким образом, чтобы каждый член был доступен по смещению от указателя объекта, поэтому реализация offsetof даст правильный ответ.

Проблема с этим, конечно же, заключается в том, что вы должны игнорировать предупреждения, а это означает, что если вы сделаете что-то, что не работает, вы разыменуете указатель, близкий к нулю. С другой стороны, причина проблемы, вероятно, будет очевидна во время выполнения. С другой стороны, eeew.

[Изменить: я только что протестировал это на gcc 3.4.4, и на самом деле предупреждение обновляется до ошибки при получении смещения члена, унаследованного от виртуального базового класса. Что приятно. Я все еще был бы немного обеспокоен тем, что будущая версия gcc (даже 4, которую мне не нужно передавать) будет более строгой, и что, если вы воспользуетесь этим подходом, ваш код может в будущем перестать компилироваться.]

person Steve Jessop    schedule 07.10.2008
comment
Спасибо! Я работаю с gcc 4.2.3 и, насколько я могу судить, он примерно такой же строгий; Получаю много предупреждений, ошибок пока нет. Он работает для более простых классов, но я еще не разобрался с классами с ABC (поэтому класс для хранения является производным от ABC, а не членами). Будет обновляться, когда / если он работает. - person yungchin; 08.10.2008

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

person Roel    schedule 07.10.2008

Вы можете объявить типы POD в базовом классе, а затем расширить этот класс (возможно, с наследованием private), чтобы добавить свои дополнительные функции.

Обновите свое обновление: поскольку экземпляр derived_non_pod также может рассматриваться как base_pod, поэтому смещения элементов данных должны быть одинаковыми. Что касается реализации, ваш компилятор разместит указатель vtable после полей base_pod при компоновке структуры derived_non_pod.

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

person Greg Hewgill    schedule 07.10.2008
comment
Привет спасибо! Это абсолютно чистое решение для примера, который я открыл, и я об этом не подумал. Возможно, он не масштабируется для случая, когда у вас есть члены CompType (составного типа) внутри других CompTypes (я добавлю к вопросу). - person yungchin; 08.10.2008
comment
Спасибо, что вернулись и добавили объяснение-обновление, теперь я его понял. Потрясающий. - person yungchin; 08.10.2008
comment
(Временно?) Проголосовали против. Компилятору разрешено на 100% добавлять vptr по нулевому смещению в производном классе, отличном от POD; фактически, компиляторы IIRC, использующие интерфейс EDG (например, Green Hills), делают именно это. Это означает, что преобразование от base_pod к derived_non_pod (или наоборот) должно добавить 4 или 8 байтов к необработанному значению указателя; но, конечно, это не сложнее, чем обычный сценарий множественного наследования. Мотивация: размещение vptr со смещением 0 означает, что горячий путь для вызова виртуальной функции (на RISC-машине) становится короче на одну add инструкцию. - person Quuxplusone; 27.03.2014

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

struct A
{
  int i;
};

class B: public A
{
public:
  virtual void foo ()
  {
  }
};

int main ()
{
  std::cout << offsetof (B, A::i) << std::endl;
}

С g ++ вышеприведенное выводит 4, чего и следовало ожидать, если у B есть vtable перед членом базового класса 'i'.

Однако должно быть возможно вычислить смещение вручную, даже для случая, когда есть виртуальные базы:

struct A1 {
  int i;
};

struct A2 {
  int j;
};

struct A3 : public virtual A2 {
};

class B: public A1, public A3 {
public:
  virtual void foo () {
  }
};

template <typename MostDerived, typename C, typename M>
ptrdiff_t calcOffset (M C::* member)
{
  MostDerived d;
  return reinterpret_cast<char*> (&(d.*member)) - reinterpret_cast<char*> (&d);
}

int main ()
{
  B b;
  std::cout << calcOffset<B> (&A2::j) << ", " 
            << calcOffset<B> (&A1::i) << std::endl;
}

С g ++ эта программа выводит 4 и 8. Это снова согласуется с vtable как первым членом B, за которым следует виртуальная база A2 и ее член 'j'. Наконец, невиртуальная база A1 и ее член «i».

Ключевым моментом является то, что вы всегда рассчитываете смещения на основе самого производного объекта, т.е. B. Если участники являются частными, вам может потребоваться добавить вызов getMyOffset для каждого члена. Этот вызов выполнит вычисление там, где имя доступно.

Вы также можете найти следующие полезные. Думаю, неплохо связать все это с объектом, для которого вы создаете тип HDF:

struct H5MemberDef
{
  const char * member_name;
  ptrdiff_t offset;
  H5PredType h5_type;
};


class B  // ....
{
public:

  // ...

  static H5memberDef memberDef[];
};

H5MemberDef B::memberDef[] = {
  { "i", calcOffset<B> (&A1::i), H5::PredType::NATIVE_INT }
  , { "j", calcOffset<B> (&A2::j), H5::PredType::NATIVE_INT }
  , { 0, 0, H5::PredType::NATIVE_INT }
};

И затем вы можете построить H5type с помощью цикла:

H5::CompType func_that_creates_example_CompType(H5MemberDef * pDef) {
  H5::CompType ct;
  while (*pDef->member_name != 0)
  {
    ct.insertMember(pDef->member_name, pDef->offset, pDef->h5_type);
    ++pDef;
  }
  return ct;
}

Теперь, если вы добавите элемент в B или одну из его баз, то простое добавление в эту таблицу приведет к созданию правильного типа HDF.

person Richard Corden    schedule 05.08.2009

Проблема в том, что как только ваша структура / класс не является внешним "C", компиляторы C ++ могут свободно переупорядочивать и оптимизировать макет вашей структуры / класса, поэтому вы можете получить переупорядоченную структуру в зависимости от вашего компилятора.

Существуют флаги препроцессора (например, #pragma pack) для поведения, подобного C, но в большинстве случаев они не переносятся.

person Fionn    schedule 07.10.2008
comment
Небольшая вещь: перестановка применяется ко всем структурам C. Способ организации переменных-членов в структуре в C (или C ++) полностью зависит от реализации компилятора. - person Dan Cristoloveanu; 07.10.2008
comment
Есть некоторые ограничения. Например, 9.2 (12) говорит, что члены, объявленные без промежуточного спецификатора доступа, появляются в памяти в объявленном порядке (возможно, с промежуточными байтами). Таким образом, повторный заказ можно предотвратить. Однако полная зависимость от компилятора - это хорошее практическое правило. - person Steve Jessop; 07.10.2008
comment
Это интересно - хотя компилятор не может полностью переупорядочивать элементы (как было отмечено одним из участников), я был весьма удивлен, что переупорядочение элементов данных было разрешено вообще. Я узнал что-то новое сегодня. - person Michael Burr; 07.10.2008

Будет ли работать указатель на член вместо offsetof ()? Я знаю, что вам, вероятно, придется выполнять всевозможные преобразования, чтобы иметь возможность фактически использовать указатель, поскольку я предполагаю, что InsertMember действует с типом, указанным в последнем параметре, во время выполнения.

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

person Michael Burr    schedule 07.10.2008
comment
Привет спасибо! Я думаю, что в этом случае это не сработает, потому что CompType :: insertMember () не принимает экземпляр класса, а просто берет байтовое смещение члена. Тип в последнем параметре описывает структуру нового члена; это также может быть другой CompType (для композиции). - person yungchin; 08.10.2008

Это отлично работает для меня, и я не понимаю, почему это не так:

#define myOffset(Class,Member) ({Class o; (size_t)&(o.Member) - (size_t)&o;})
person Greg Slepak    schedule 29.04.2013
comment
Он не будет работать с компиляторами, которые не поддерживают расширение выражения оператора GNU (например, Clang и MSVC). Кроме того, оптимизатор может изменить это вычитание на константу 0, поскольку он вызывает неопределенное поведение. - person Quuxplusone; 26.03.2014
comment
Спасибо за информацию @Quuxplusone! - person Greg Slepak; 28.03.2014