null

Знакомимся с boost::interprocess::offset_ptr

Доброе утро!

В этой статье я хочу в кратце осветить принципы работы одного из умных указателей, предоставленных нам Boost: offset_ptr.

Этот прекрасный класс создан для использования в условиях разделяемой памяти. Представим такую задачу: у нас есть набор структур данных в памяти, указывающих друг на друга, и мы хотим, чтобы они все находились в памяти, которая отображена в некоторый файл. Соответственно, при старте приложения этот файл должен отображаться в память (mmap(2)), и программа должна продолжать работать с этими структурами без всякой десериализации.

Что будет, когда мы отобразим в память файл в общем случае? Он будет отображён на какой-то заранее неизвестный адрес. Вспомним, что структуры указывали друг на друга, соответственно, их абсолютные адреса поменялись и мы получаем набор невалидных указателей. Тут на сцену и выходит offset_ptr, полагающийся не на абсолютные адреса, а не относительные.

offset_ptr — это класс умного указателя, находящийся в boost::interprocess. Он имеет тот же интерфейс, что и остальные умные указатели, но при этом хранит в себе не адрес объекта, на который должен указывать, а разницу между адресом и своим this, что позволяет вычислять адрес объекта при условиях, что указатель и объект указания (pointer и pontee) находятся в одном блоке «двигаемой» памяти, то есть, расстояние между ними не меняется.

Важная проблема -- представление nullptr в таком указателе. Хранить в качестве сдвига 0 не вариант, так как тогда указатель не сможет показывать на себя (а это необходимо в реализации некоторых структур данных). Разработчики Boost пошли по не очень красивому, но работающему пути: они справедливо положили, что адрес, находящийся на один байт после адреса указателя вряд ли кому-то понадобится, а значит nullptr в их представлении -- это ((char*)this)+1.

Как следствие, нам не нужно использовать ничего сложного для работы с относительными адресами, а просто использовать «обычный» умный указатель примерно таким образом:

struct a {
  int a;
  boost::interprocess::offset_ptr<int> ptr;
  int b;
}

a obj;
// Take the first integer's pointer, count difference,
// store "-4" into a.ptr
a.ptr = &a.a;
// do something
// Store "4" into a.ptr
a.ptr = &a.b;
// do something else

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

 

Теперь немножко поговорим о реализации.

Реализован он в boost/interprocess/offset_ptr.hpp. Для начала взглянем на безаргументный конструктор:

   //!Default constructor (null pointer).
   //!Never throws.
   offset_ptr()
      : internal(1)
   {}

Всё, что он делает — инициализирует единицей поле, упрощённо представляющее собой сдвиг, что значит, что по-умолчанию эти указатели указывают на null.

А теперь взглянем на конструктор, вызываемый при создании из «сырого» указателя:

   //!Constructor from raw pointer (allows "0" pointer conversion).
   //!Never throws.
   offset_ptr(pointer ptr)
      : internal(static_cast<OffsetType>(ipcdetail::offset_ptr_to_offset<0>(ptr, this)))
   {}

Он обращается к функции offset_ptr_to_offset(). Рассмотрим её основную часть:

         if(!ptr){
            return 1;
         }
         else{
            const caster_t this_caster((void*)this_ptr);
            const caster_t ptr_caster((void*)ptr);
            std::size_t offset = ptr_caster.size() - this_caster.size();
            BOOST_ASSERT(offset != 1);
            return offset;
         }

Для NULL она возвращает единицу, как и описано выше, а для остальных значений — просто вычисляет разницу между указателями, используя обёртку, преобразующую их к size_t.

Операторы разименования, индексации и подобного реализованы через get(), метод, предоставляющий доступ к «сырому» указателю (выполняющий обратное преобразование, очень похожее на рассмотренное выше), ну и всевозможные операторы присваивания и адресной арифметики выполняются через вычисление нового сдвига.

Стоит также заметить, что offset_ptr, в отличие от собратьев shared_ptr и unique_ptr, не занимается управлением памятью, так что уничтожение его объекта не влечёт за собой уничтожение того, на кого он указывает. Следственно, за памятью надо следить самим.

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