Нужно ли сделать тип POD, чтобы сохранить его с отображенным в память файлом - PullRequest
15 голосов
/ 04 сентября 2011

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

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

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

Позже я нашел boost::interprocess::offset_ptr, создание которого было обусловлено теми же потребностями.Тем не менее, оказывается, что offset_ptr нетривиально копируется, потому что он реализует свой собственный конструктор копирования.

Мое предположение, что умный указатель должен быть тривиально копируемым, чтобы его можно было безопасно сохранить неправильно?

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

struct base {
    int x;
    virtual void f() = 0;
    virtual ~base() {} // virtual members!
};

struct derived : virtual base {
    int x;
    void f() { std::cout << x; }
};

using namespace boost::interprocess;

void persist() {
    file_mapping file("blah");
    mapped_region region(file, read_write, 128, sizeof(derived));
    // create object on a memory-mapped file
    derived* d = new (region.get_address()) derived();
    d.x = 42;
    d->f();
    region.flush();
}

void retrieve() {
    file_mapping file("blah");
    mapped_region region(file, read_write, 128, sizeof(derived));
    derived* d = region.get_address();
    d->f();
}

int main() {
    persist();
    retrieve();
}

Спасибо всем тем, кто предоставил альтернативы.Вряд ли я буду использовать что-то еще в ближайшее время, потому что, как я объяснил, у меня уже есть работающее решение.И как вы можете видеть из использования вопросительных знаков выше, мне действительно интересно узнать, почему Boost может обойтись без тривиально копируемого типа и как далеко вы можете зайти с ним: совершенно очевидно, что классы с виртуальными членами не будутработать, но где вы рисуете линию?

Ответы [ 6 ]

7 голосов
/ 07 сентября 2011

Чтобы избежать путаницы, позвольте мне повторить проблему.

Вы хотите создать объект в отображенной памяти таким образом, чтобы после закрытия и повторного открытия приложения файл можно было сопоставить еще раз, а объект использовать бездальнейшая десериализация.

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

Независимость от адреса требует от вас:

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

Из этих правил следует несколько корреляций.

  • Вы не можете использовать virtual что-либо.Виртуальные функции C ++ реализованы с помощью скрытого указателя vtable в экземпляре класса.Указатель vtable является абсолютным указателем, над которым у вас нет никакого контроля.
  • Вы должны быть очень осторожными в отношении других объектов C ++, используемых вашими объектно-независимыми объектами.В основном все в стандартной библиотеке может сломаться, если вы их используете.Даже если они не используют new, они могут использовать виртуальные функции для внутреннего использования или просто сохранить адрес указателя.
  • Нельзя хранить ссылки в объектах, не зависящих от адреса.Ссылочные члены являются просто синтаксическим сахаром над абсолютными указателями.

Наследование все еще возможно, но имеет ограниченную полезность, так как виртуальное запрещено.

Любой и все конструкторы / деструкторы хороши, покаПриведенные выше правила соблюдаются.

Даже Boost.Interprocess не идеально подходит для того, что вы пытаетесь сделать.Boost.Interprocess также должен управлять общим доступом к объектам, в то время как вы можете предположить, что вы только один возитесь с памятью.

В конце может быть проще / разумнее просто использовать GoogleПротобуфы и обычная сериализация.

4 голосов
/ 04 сентября 2011

Да, но не по тем причинам, которые вас беспокоят.

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

Если вы хотите использовать этот стиль настойчивости, вам нужно отказаться от «виртуального».После этого все дело в семантике.В самом деле, просто притворись, что ты делал это в C.

2 голосов
/ 05 сентября 2011

Не столько ответ, сколько комментарий, который стал слишком большим:

Я думаю, это будет зависеть от того, сколько безопасности вы готовы обменять на скорость / простоту использования.В случае, если у вас есть struct, как это:

struct S { char c; double d; };

Вы должны учитывать заполнение и тот факт, что некоторые архитектуры могут не позволить вам получить доступ к double, если он не выровнен по правильномуадрес памяти.Добавление функций доступа и исправление отступов решают эту проблему, и структура по-прежнему memcpy -поглощаема, но теперь мы входим на территорию, где на самом деле мы не получаем большой выгоды от использования файла отображения памяти.

Так как кажется, что вы будете использовать это только локально и в фиксированной настройке, ослабление требований кажется немного нормальным, поэтому мы вернулись к использованию выше struct в обычном режиме.Теперь функция должна быть тривиально копируемой?Я не обязательно так думаю, рассмотрите этот (возможно, неработающий) класс:

   1 #include <iostream>
   2 #include <utility>
   3 
   4 enum Endian { LittleEndian, BigEndian };
   5 template<typename T, Endian e> struct PV {
   6         union {
   7                 unsigned char b[sizeof(T)];
   8                 T x;
   9         } val;  
  10         
  11         template<Endian oe> PV& operator=(const PV<T,oe>& rhs) {
  12                 val.x = rhs.val.x;
  13                 if (e != oe) {
  14                         for(size_t b = 0; b < sizeof(T) / 2; b++) {
  15                                 std::swap(val.b[sizeof(T)-1-b], val.b[b]);
  16                         }       
  17                 }       
  18                 return *this;
  19         }       
  20 };      

Это не просто копировать, и вы не можете просто использовать memcpy, чтобы переместить его в целом, но я неС использованием подобного класса в контексте отображенного в памяти файла не может быть ничего неправильного (особенно если файл не соответствует собственному порядку байтов).

Обновление:
Где вы рисуетестрока?

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

Это сделало бы boost::interprocess::offset_ptr ОК, поскольку это просто полезная оболочка для ptrdiff_t со специальными семантическими правилами.В том же духе struct PV выше было бы нормально, так как он просто предназначен для автоматической перестановки байтов, хотя, как и в C, вы должны быть осторожны, чтобы отслеживать порядок байтов и предполагать, что структура может быть просто скопирована.Виртуальные функции не будут в порядке, так как эквивалент C, указатели функций в структуре, не будет работать.Однако что-то вроде следующего (непроверенного) кода снова будет в порядке:

struct Foo { 
    unsigned char obj_type;
    void vfunc1(int arg0) { vtables[obj_type].vfunc1(this, arg0); }
};
2 голосов
/ 04 сентября 2011

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

Вы можете посмотреть Буферы протокола Google , чтобы найти способ сделать это в переносном режиме.

1 голос
/ 05 сентября 2011

Абсолютно нет. Сериализация - это хорошо зарекомендовавшая себя функциональность, которая используется во многих ситуациях и, конечно, не требует POD. Для этого требуется, чтобы вы указали четко определенный двоичный интерфейс сериализации (SBI).

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

Где помощь POD - это когда вы знаете, что не покидаете архитектуру процессора. Если вы никогда не будете менять версии между авторами объекта (сериализаторами) и читателями (десериализаторами) и вам не нужны данные динамического размера, тогда POD позволяют легко сериализировать на основе memcpy.

Обычно, однако, вам нужно хранить такие вещи, как строки. Затем вам нужен способ хранения и извлечения динамической информации. Иногда используются 0 завершенных строк, но это довольно специфично для строк и не работает для векторов, карт, массивов, списков и т. Д. Вы часто будете видеть строки и другие динамические элементы, сериализованные как [размер] [элемент 1] [элемент 2] ... это формат массива Pascal. Кроме того, при работе с межмашинными коммуникациями ВОО должен определить интегральные форматы для решения потенциальных проблем с порядком байтов.

Теперь указатели обычно реализуются с помощью идентификаторов, а не смещений. Каждому объекту, который должен быть сериализован, может быть присвоен увеличивающийся номер в качестве идентификатора, и это может быть первое поле в SBI. Причина, по которой вы обычно не используете смещения, заключается в том, что вы не сможете легко рассчитать будущие смещения без прохождения шага определения размера или второго прохода. Идентификаторы могут быть вычислены внутри подпрограммы сериализации при первом проходе.

Дополнительные способы сериализации включают в себя текстовые сериализаторы с использованием некоторого синтаксиса, такого как XML или JSON. Они анализируются с использованием стандартных текстовых инструментов, которые используются для реконструкции объекта. Это делает SBI простым за счет снижения производительности и пропускной способности.

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

Это часто выглядит как

void MyClass::Serialise(SerialisationStream & stream)
{
  stream & member1;
  stream & member2;
  stream & member3;
  // ...
}

где оператор & перегружен для ваших различных типов. Вы можете взглянуть на boost.serialize для большего количества примеров.

1 голос
/ 04 сентября 2011

Это не сработает.Ваш class Derived не является POD, поэтому от того, как он компилирует ваш код, зависит компилятор.Другими словами - не делайте этого.

Кстати, куда вы выпускаете свои объекты?Я вижу, создаются ваши объекты на месте, но вы не вызываете деструктор.

...