Должен ли я сделать тип POD для сохранения его с помощью файла с отображением памяти?
Указатели не могут сохраняться непосредственно в файле, поскольку они указывают на абсолютные адреса. Чтобы решить эту проблему, я написал шаблон relative_ptr
, который содержит смещение вместо абсолютного адреса.
Исходя из того факта, что только тривиально скопируемые типы могут быть безопасно скопированы поэтапно, я сделал предположение, что этот тип должен быть тривиально скопируемым, чтобы быть надежно сохраненным в файле с отображением памяти и извлеченным позже.
Это ограничение оказалось немного проблематичным, потому что созданный компилятором конструктор копий не ведет себя значимым образом. Я не нашел ничего, что запрещало бы мне дефолтировать конструктор копирования и сделать его закрытым, поэтому я сделал его конфиденциальным, чтобы избежать случайных копий, которые приведут к поведению undefined.
Позже я нашел 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 может уйти без тривиально-скопируемого типа и как далеко вы можете с ним справиться: совершенно очевидно, что классы с виртуальными членами не будут работа, но , где вы рисуете линию?
Ответы
Ответ 1
Чтобы избежать путаницы, позвольте мне повторить проблему.
Вы хотите создать объект в сопоставленной памяти таким образом, чтобы после закрытия приложения и его повторного открытия файл можно снова отобразить и использовать объект без дальнейшей десериализации.
POD - это своего рода красная селедка за то, что вы пытаетесь сделать. Вам не нужно бинарно копировать (что означает POD); вам необходимо быть независимым от адреса.
Независимость адресов требует от вас:
- избегать всех абсолютных указателей.
- используют только указатели смещения по адресам в отображаемой памяти.
Есть несколько корреляций, которые следуют из этих правил.
- Вы не можете использовать
virtual
что угодно. Виртуальные функции С++ реализованы с помощью скрытого указателя vtable в экземпляре класса. Указатель vtable является абсолютным указателем, над которым у вас нет никакого элемента управления.
- Вам нужно быть очень осторожным в отношении других объектов С++, которые используют ваши объекты, не зависящие от адреса. В основном все в стандартной библиотеке может сломаться, если вы их используете. Даже если они не используют
new
, они могут использовать виртуальные функции внутри или просто сохранить адрес указателя.
- Вы не можете хранить ссылки в объектах, не зависящих от адреса. Ссылочные элементы - это просто синтаксический сахар по абсолютным указателям.
Наследование все еще возможно, но имеет ограниченную полезность, поскольку виртуальный запрещен.
Любые и все конструкторы/деструкторы прекрасны, если соблюдаются приведенные выше правила.
Даже Boost.Interprocess не подходит для того, что вы пытаетесь сделать. Boost.Interprocess также должен управлять совместным доступом к объектам, тогда как вы можете предположить, что вы только один messing с памятью.
В конце концов, проще всего использовать Google Protobufs и обычную сериализацию.
Ответ 2
Да, но по причинам, отличным от тех, которые вас беспокоят.
У вас есть виртуальные функции и виртуальный базовый класс. Это приводит к созданию множества указателей, созданных за спиной компилятором. Вы не можете превратить их в смещения или что-то еще.
Если вы хотите сделать этот стиль упорства, вам нужно отказаться от "виртуального". После этого все дело в семантике. Действительно, просто притворись, что делаешь это на С.
Ответ 3
Даже у PoD есть проблемы, если вы заинтересованы в взаимодействии в разных системах или во времени.
Вы можете посмотреть Google Protocol Buffers, чтобы сделать это переносимым способом.
Ответ 4
Не так много ответа, как комментарий, который стал слишком большим:
Я думаю, что это будет зависеть от того, сколько безопасности вы готовы торговать за скорость/простоту использования. В случае, если у вас есть struct
:
struct S { char c; double d; };
Вам нужно учитывать дополнение и тот факт, что некоторые архитектуры могут не позволить вам получить доступ к double
, если он не выровнен по правильному адресу памяти. Добавление функций доступа и исправление дополнений устраняет это, и структура все еще memcpy
-able, но теперь мы входим на территорию, где мы на самом деле не получаем большую пользу от использования файла с отображением памяти.
Так как кажется, что вы будете использовать это локально и в фиксированной настройке, расслабляя требования, немного кажется ОК, поэтому мы вернемся к использованию вышеуказанного 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-код является приемлемым, а С++ просто используется в качестве удобства, для обеспечения безопасности типа или надлежащего доступа, это должно быть хорошо.
Это сделало бы 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); }
};
Ответ 5
Это не сработает. Ваш class Derived
не является POD, поэтому он зависит от компилятора, как он компилирует ваш код. Другими словами - не делайте этого.
кстати, где вы выпускаете свои объекты? Я вижу, что вы создаете свои объекты на месте, но вы не вызываете деструктор.
Ответ 6
Абсолютно нет. Сериализация - это хорошо зарекомендовавшая себя функциональность, которая используется во многих ситуациях и, конечно же, не требует POD. Для этого требуется указать четко определенный двоичный интерфейс сериализации (SBI).
Сериализация необходима в любое время, когда ваши объекты покидают среду выполнения, включая общую память, каналы, сокеты, файлы и многие другие механизмы сохранения и связи.
Где помощь PODs - это то, где вы знаете, что вы не покидаете архитектуру процессора. Если вы никогда не будете менять версии между писателями объекта (сериализаторы) и считывателями (десеризаторы), и вам не нужны данные с динамическим размером, тогда POD разрешают простые сериализаторы на основе memcpy.
Как правило, вам нужно хранить такие вещи, как строки. Затем вам нужен способ хранения и получения динамической информации. Иногда используются 0 завершенных строк, но это довольно специфично для строк и не работает для векторов, карт, массивов, списков и т.д. Вы часто увидите строки и другие динамические элементы, сериализованные как [размер] [элемент 1] [element 2]... это формат массива Pascal. Кроме того, когда речь идет о перекрестных машинных сообщениях, ВОО должен определить интегральные форматы для решения потенциальных проблем, стоящих перед лицом.
Теперь указатели обычно реализуются с помощью идентификаторов, а не смещений. Каждому объекту, который должен быть сериализован, может быть присвоен инкрементирующий номер как идентификатор, и это может быть первое поле в ВОО. Причина, по которой вы обычно не используете смещения, состоит в том, что вы не сможете легко рассчитать будущие смещения, не пройдя шаг калибровки или второй проход. Идентификаторы могут рассчитываться внутри процедуры сериализации на первом проходе.
Дополнительные способы сериализации включают сериализаторы на основе текста с использованием некоторого синтаксиса, такого как XML или JSON. Они анализируются с использованием стандартных текстовых инструментов, которые используются для восстановления объекта. Они обеспечивают простоту SBI за счет пессимизации производительности и пропускной способности.
В конце концов, вы, как правило, создаете архитектуру, в которой вы создаете потоки сериализации, которые берут ваши объекты и переводят их член по члену в формат вашего ВОО. В случае разделяемой памяти он обычно подталкивает членов непосредственно к памяти после получения общего мьютекса.
Это часто выглядит как
void MyClass::Serialise(SerialisationStream & stream)
{
stream & member1;
stream & member2;
stream & member3;
// ...
}
где оператор и перегружен для разных типов. Вы можете взглянуть на boost.serialize для получения дополнительных примеров.