В текущем проекте для передачи данных между процессами используется разделяемая память (она же shared-memory, реализованная посредством memory-mapped files).
В основном через эту память передаются большие объемы "сырых" данных. Так что особой надобности размещать в shared-memory каких-то С++ных объектов пока не было. За исключением простого заголовка, который предшествует этим самым "сырым" данным.
Однако, на горизонте начинает маячить сценарий, при котором через shared memory может потребоваться передавать небольшие и (пока?) несложные C++ные объекты с управляющей информацией.
И тут на глаза попадается свежая статья на Хабре: Динамические структуры в shared-памяти. Любопытный подход к проблеме там описан.
В код упомянутой в статье библиотеки особо не вникал, просто просмотрел бегло по диагонали. Качеством кода не впечатлился. Но задумался о чем-то подобном.
Однако, как я понимаю, в рамках C++17 нет легальных способов взять указатель на какую-то последовательность байт в разделяемой памяти и сказать, что это вот такой-то C++ный объект. Поскольку все это сродни вот такому трюку:
struct my_object { int x; }
alignas(my_object) char buffer[sizeof(my_object)];
auto * p = reinterpret_cast<my_object *>(buffer);
p->x = 1; // Undefined behavior!
И в C++17, и (насколько я помню) в C++20, подобный код не валиден, т.к. компилятор не видит конструирования объекта, на который должен указывать p. Поэтому обращение по p являются undefined behavior. Подробности можно прочитать вот в этом обсуждении на Reddit.
Подозреваю, что сейчас большинство компиляторов допускают использование такого трюка. Но UB в коде чревато.
Поиск привел вот на такой документ: P1631R1: Object detachment and attachment. Но складывается ощущение, что там еще и конь не валялся.
Upd. Есть еще вот такой документ: P0593. Однако, как я понял, именно для работы с объектами в разделяемой памяти очень желательно иметь упомянутую в этом документе std::start_lifetime_as, которой пока в C++ нет :(
Upd2. Интересный материал был найден здесь: https://blog.panicsoftware.com/objects-their-lifetimes-and-pointers/ (но у меня именно этот URL сейчас не открывается, поэтому читал сохраненную в web.archive копию.
В общем, если кто-то из читателей может накидать ссылок на документы/обсуждения, в которых говорится о том, как же правильно и легально можно работать с C++ными объектами, размещенными в разделяемой памяти, то буду признателен.
ПРИМЕЧАНИЕ. Написанное ниже курсивом "решение" в принципе не рабочее, можно не обращать на него внимание.
У меня в качестве промежуточной идеи в голове крутится мысль о том, чтобы использовать placement new для объектов. Допустим, у нас есть что-то вроде:
struct data { int x_; int y_; ... };
Эту структуру мы снабжаем двумя конструкторами. Первый полностью инициализирует data и предназначен для использования в процессе, который создает объект в разделяемой памяти:
// Конструктор для процесса-продюсера. data::data(int x, int y) : x_{x}, y_{y} {} ... // Получаем место в разделяемой памяти под новый объект. void * shmem_block = alloc_block_in_shared_memory(sizeof(data)); // Конструируем объект в разделяемой памяти. auto * d = new(shmem_block) data{0, 1}; ...
Второй конструктор предназначен для использования в процессе, который должен использовать "чужие" объекты в разделяемой памяти. Этот конструктор ничего не делает.
Вообще ничего, никакой инициализации. В предположении, что в качестве значений для полей объекта будет взято содержимое памяти, на которую "ляжет" новый экземпляр:
// Конструктор для процесса-консюмера. data::data() {} ... // Как-то получаем указатель на разделяемую память. void * shmem_block = receive_shared_memory_ptr(); // Типа конструируем объект, который уже есть в разделяемой памяти. auto * d = new(shmem_block) data(); // Теперь используем содержимое объекта. auto x = d.x_ + k / d.y_;
Но, подозреваю, что здесь будут свои UB: как минимум, это должны быть обращения к полям объекта, которые не были проинициализированны в конструкторе. Плюс к тому, для указателя d, который мы получили через placement new, не будет вызываться delete. Т.е. мы не вызываем деструктор для сконструированного объекта, что не должно быть правильно с точки зрения стандарта.
К сожалению, знатоком C++ного стандарта никогда не был, поэтому не могу подтвердить (или опровергнуть) свои предположения ссылками на конкретные разделы стандарта. Может кто-то из читателей сталкивался с такими вещами и может поделиться опытом?
В общем, пока что самый надежный способ -- это тупая сериализация/десериализация. Чем и пользуемся. Но было бы интересно получить и какое-то другое решение.
Комментариев нет:
Отправить комментарий