Может ли вектор Boost Container управлять памятью с помощью не необработанных указателей?

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

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

Теперь я хочу использовать эти указатели для управления STL-подобным контейнером. С самого начала я понял, что вектор STL в принципе не может обрабатывать несырые указатели. T* слишком жестко закодирован, и стандарт в основном исключает все, что не является указателем.

Вдохновленный Boost.Interprocess offset_ptr<T>, я решил использовать Boost.Container vector, который легко настраивается и, в принципе, может управлять чем угодно, распределитель, переданный boost::container::vector, может обрабатывать все, что похоже на указатель.

Теперь класс boost::container::vector<T, myallocator_with_special_pointer<T>> может делать что угодно... кроме resize()!!

Глядя на код в boost/container/vector.hpp, кажется, что процесс изменения размера (который в основном представляет собой и выделение, за которым следует копирование (или перемещение) и освобождение) включает необработанные указатели.

Оскорбительная строка:

  [line 2729:] T * const new_buf = container_detail::to_raw_pointer
     (allocator_traits_type::allocate(this->m_holder.alloc(), new_cap, this->m_holder.m_start));

За которым позже следует

  [line 3022:] this->m_holder.start(new_start);  // new_start is the same as new_buf above. 
  // member ::start(pointer&) will need to convert a raw pointer to the pointer typedef.

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

Кажется довольно глупым, что эта маленькая деталь запрещает использование несырых указателей. Учитывая все усилия, чтобы контейнер был общим (например, определение типа pointer), почему эта часть кода использует T* только для изменения размера?

Другими словами, почему Boost Container не использует эту строку вместо

  [alternative] pointer const new_buf = 
     allocator_traits_type::allocate(this->m_holder.alloc(), new_cap, this->m_holder.m_start);

Есть ли обходной путь или альтернативный способ использования вектора Boost Container для обработки не необработанных указателей?

Boost.Container говорит на своей странице руководства http://www.boost.org/doc/libs/1_64_0/doc/html/container/history_and_reasons.html#container.history_and_reasons.Why_boost_container

Boost.Container — это продукт длительной разработки, которая началась в 2004 году с экспериментальной библиотеки Shmem, впервые использовавшей стандартные контейнеры в разделяемой памяти. Shmem включил модифицированный код контейнера SGI STL, настроенный для поддержки не необработанных типов allocator::pointer и распределителей с отслеживанием состояния. После рассмотрения Shmem был принят как Boost.Interprocess, и эта библиотека продолжала уточнять и улучшать эти контейнеры.

Текущая реализация (в контексте изменения размера) идет вразрез с этой целью дизайна.


Я задал здесь менее конкретный вопрос о других характеристиках распределителей: type/41581304#41581304">Есть ли возможность настроить тип ссылки вектора STL?


Для справки, распределитель, который указывает специальный указатель (который распространяется на контейнер), выглядит примерно так:

template<class T>
struct allocator{
    using value_type = T;
    using pointer = array_ptr<T>; // simulates T*
    using const_pointer = array_ptr<T const>; // simulates T const*
    using void_pointer = array_ptr<void>; // simulates void*
    using const_void_pointer = array_ptr<void const>; // simulates void const*
    some_managed_shared_memory& msm_;
    allocator(some_managed_shared_memory& msm) : msm_(msm){}
    array_ptr<T> allocate(mpi3::size_t n){
        auto ret = msm_.allocate(n*sizeof(T));
        return static_cast<array_ptr<T>>(ret);
    }
    void deallocate(array_ptr<T> ptr, mpi3::size_t = 0){
        msm_.deallocate(ptr);
    }
};

Полный рабочий код http://coliru.stacked-crooked.com/a/f43b6096f9464cbf

#include<iostream>
#include <boost/container/vector.hpp>

template<typename T>
struct array_ptr;

template<>
struct array_ptr<void> {
    using T = void;
    T* p;
    int i; //some additional information

//    T& operator*() const { return *p; }
    T* operator->() const { return p; }

//    operator T*() const { return p; }
    template<class TT>
    operator array_ptr<TT>() const{return array_ptr<TT>((TT*)p, i);}
    operator bool() const{return p;}
    array_ptr(){}
    array_ptr(std::nullptr_t) : p(nullptr){}
    array_ptr(T* ptr, int _i) : p(ptr), i(_i){}
    template<class Other>
    array_ptr(array_ptr<Other> other) : p(other.p), i(other.i){}
};

template<>
struct array_ptr<void const> {
    using T = void const;
    T* p;
    int i; //some additional information

//    T& operator*() const { return *p; }
    T* operator->() const { return p; }

    operator T*() const { return p; }
    array_ptr(){}
    array_ptr(std::nullptr_t) : p(nullptr){}
    array_ptr(T* ptr, int _i) : p(ptr), i(_i){}
    template<class Other>
    array_ptr(array_ptr<Other> other) : p(other.p), i(other.i){}
};

template<typename T>
struct array_ptr {
    T* p;
    int i; //some additional information

    T& operator*() const { return *p; }
    T* operator->() const { return p; }
    T& operator[](std::size_t n) const{
        assert(i == 99);
        return *(p + n);
    }
    bool operator==(array_ptr const& other) const{return p == other.p and i == other.i;}
    bool operator!=(array_ptr const& other) const{return not((*this)==other);}

//    operator T*() const { return p; }
    array_ptr& operator++(){++p; return *this;}
    array_ptr& operator+=(std::ptrdiff_t n){p+=n; return *this;}
    array_ptr& operator-=(std::ptrdiff_t n){p-=n; return *this;}
    array_ptr operator+(std::size_t n) const{array_ptr ret(*this); ret+=n; return ret;}
    std::ptrdiff_t operator-(array_ptr const& other) const{return p - other.p;}
    array_ptr(){}
    array_ptr(std::nullptr_t) : p(nullptr), i(0){}

    operator bool() const{return p;}

    array_ptr(T* ptr, int _i) : p(ptr), i(_i){}
    array_ptr(T* ptr) : p(ptr), i(0){}
    array_ptr(int) : p(nullptr), i(0){}
    array_ptr(array_ptr<void> const& other) : p(static_cast<T*>(other.p)), i(other.i){}
};

struct some_managed_shared_memory {
    array_ptr<void> allocate(size_t n) { return array_ptr<void>(::malloc(n), 99); }
    void  deallocate(array_ptr<void> ptr) { if (ptr) ::free(ptr.p); }
};

template<typename T>
struct allocator{
    using value_type = T;
    using pointer = array_ptr<T>; // simulates T*
    using const_pointer = array_ptr<T const>; // simulates T const*
    using void_pointer = array_ptr<void>; // simulates void*
    using const_void_pointer = array_ptr<void const>; // simulates void const*

    some_managed_shared_memory& msm_;
    allocator(some_managed_shared_memory& msm) : msm_(msm){}
    array_ptr<T> allocate(size_t n){
        auto ret = msm_.allocate(n*sizeof(T));
        return static_cast<array_ptr<T>>(ret);
    }
    void deallocate(array_ptr<T> ptr, std::size_t = 0){
        msm_.deallocate(ptr);
    }
};

int main() {
    some_managed_shared_memory realm;
    boost::container::vector<int, allocator<int> > v(10, realm);
    assert( v[4] == 0 );
    v[4] = 1;
    assert( v[4] == 1 );
    for(std::size_t i = 0; i != v.size(); ++i) std::cout << v[i] << std::endl;
    for(auto it = v.begin(); it != v.end(); ++it) std::cout << *it << std::endl;

    // none of these compile:
    v.push_back(8);
    assert(v.size() == 11);
    v.resize(100);
    std::cout << v[89] << std::endl; // will fail an assert because the allocator information is lost
    //v.assign({1,2,3,4,5});
}

person alfC    schedule 06.07.2017    source источник
comment
Я думаю, что это ошибка QoI, которую следует адресовать мейнтейнерам Boost Container. Кажется, я уже видел, как они исправляли такие глюки раньше.   -  person sehe    schedule 07.07.2017
comment
@sehe Это успокаивает меня, что я не совсем отключился. Интересно, как boost::interprocess::vector (который, я думаю, также использует Boost Container) решает эту проблему. (сопутствующий код очень трудно читать). Возможно, это проще обойти для offset_ptr. Также это не похоже на простую ошибку, потому что to_raw_pointer вызывается явно. Это может быть незавершенный наполовину испеченный дизайн.   -  person alfC    schedule 07.07.2017
comment
@sehe, интересно, что оскорбительная строка находится в функции с именем priv_forward_range_insert_no_capacity, для которой есть три перегрузки, отправленные третьим аргументом с типами version_0 (который безоговорочно выдает в теле функции), version_1 (который, кажется, просто выделяет и освобождает, с помощью указателя T*) и version_2 (который, кажется, делает более причудливый ход, также используя указатели T*). Интересно, нужно ли мне определять класс распределителя или его свойства таким образом, чтобы эти функции не создавались. Смотрите мою правку.   -  person alfC    schedule 07.07.2017
comment
Когда я пытаюсь собрать воедино минимальный пример с добавленным вами распределителем, у меня возникает гораздо больше проблем: Жить SSCCE. Можете ли вы сделать его автономным образцом, демонстрирующим проблему?   -  person sehe    schedule 07.07.2017
comment
@sehe Ваш минимальный пример превосходен. Начнем с того, что работает, если я объявляю вектор с начальным ненулевым размером, доступ к элементу и разыменование работает. push_back или assign внутренне устанавливает код resize, поэтому я не удивлен, что они тоже не работали. Я добавил полный рабочий код (на основе вашего MWE), как видите, мне нужно определить специализацию для array_ptr<void>, которая не может быть разыменована, и мне нужно включить некоторые неявные преобразования (аналогично преобразованию для int* в void*). Спасибо за помощь.   -  person alfC    schedule 07.07.2017
comment
@sehe, я улучшил MWE, чтобы он работал с .begin(), .end() и operator[]. Еще раз препятствие .resize(). В этой упрощенной версии более очевидно, что в строке 714 vector.hpp есть значение по умолчанию, которое распространяется как T*, а не как pointer.   -  person alfC    schedule 07.07.2017
comment
@sehe, я смог скомпилировать его с помощью resize и push_back. Код компилируется и работает, но семантически некорректен, так как лишняя информация, добавляемая аллокатором, теряется в процессе изменения размера (в примере аллокатор добавляет информацию 99, но из-за конвертации эта информация в итоге теряется).   -  person alfC    schedule 07.07.2017
comment
Я пришел к аналогичным выводам в своем ответе, который я опубликовал тем временем. Однако некоторые оговорки о потере данных уже применяются к арифметическим операциям с указателем (см. мой ответ). Это может сделать это внутренней проблемой дизайна вашего распределителя, в отличие от чего-то, что только что было представлено шаблонами использования только в Boost Container.   -  person sehe    schedule 07.07.2017
comment
Давайте продолжим обсуждение в чате.   -  person alfC    schedule 07.07.2017


Ответы (1)


Я посмотрел на вещи.

TL;DR выглядит следующим образом: поддерживаются несырые указатели, но для некоторых операций требуется неявное преобразование из необработанных. Я не знаю, задумано это или нет, но, похоже, это не противоречит цели дизайна.

На самом деле это очень похоже на историю поддержки аллокаторов: контейнеры STL поддерживали пользовательские аллокаторы, но не аллокаторы с сохранением состояния (имеются в виду типы аллокаторов, не создаваемые по умолчанию).

Версии распределителя

Сначала я попробовал некоторые из версий распределителя:

using version = boost::container::version_0; // seems unsupported, really
using version = boost::container::version_1;
using version = boost::container::version_2; // does different operations

Но это не имело (решающего) эффекта. Возможно, в документации есть подсказки.

Арифметика указателя

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

std::cout << boost::container::container_detail::impl::version<allocator<int> >::value << "\n";

array_ptr<int> p;
auto rawp = boost::container::container_detail::to_raw_pointer(p);
std::cout << typeid(rawp).name() << "\n";

std::cout << typeid(p).name() << "\n";
std::cout << typeid(p + 5).name() << "\n";
std::cout << typeid(p - 5).name() << "\n";

Показывает что-то вроде¹

1
int*
array_ptr<int>
int*
int*

¹ украшен с помощью c++filt -t

Это привело меня к определению арифметики указателя:

template <typename T, typename N>
array_ptr<T> operator+(array_ptr<T> const& p, N n) { return array_ptr<T>(p.p+n, p.i); }

template <typename T>
array_ptr<T>& operator++(array_ptr<T>& p) { return ++p.p, p; }

template <typename T>
array_ptr<T> operator++(array_ptr<T>& p, int) { auto q = p.p++; return array_ptr<T>(q, p.i); }

template <typename T, typename N>
array_ptr<T> operator-(array_ptr<T> const& p, N n) { return array_ptr<T>(p.p-n, p.i); }

template <typename T>
ptrdiff_t operator-(array_ptr<T> const& a, array_ptr<T> const& b) { return a.p - b.p; }

Теперь вывод становится

1
int*
array_ptr<int>
array_ptr<int>
array_ptr<int>

Многие другие варианты использования успешно компилируются с этими определениями. Предполагая, что данные «аннотации» внутри array_pointer действительны после увеличения, они не должны терять информацию о распределителе.

Настоящий преступник

После этого некоторые вещи все еще не компилируются. В частности, в некоторых местах тип pointer распределителя создается на основе необработанного указателя. Это не удается, потому что нет подходящего конструктора преобразования "по умолчанию". Если вы объявите конструкторы с необязательным значением данных, все скомпилируется, но вы можете возразить, что при этом теряется информация, так как есть путь от

 array_pointer<T> p;
 auto* rawp = to_raw_pointer(p);
 array_pointer<T> clone(rawp); // oops lost the extra info in p

НАБЛЮДЕНИЕ

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

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

ДЕМО ВРЕМЯ

Жить на Coliru

#if COMPILATION_INSTRUCTIONS
clang++ -std=c++14 -Wall -Wfatal-errors $0 -o $0x.x && $0x.x $@ && rm -f $0x.x; exit
#endif

#define DEFAULT_DATA = 0
#define DEFINE_ARITHMETIC_OPERATIONS

#include <iostream>
#include <boost/container/vector.hpp>
#include <typeinfo>

template<typename T>
struct array_ptr {
    T* p;
    int i; //some additional information

    T& operator*() const { return *p; }
    T* operator->() const { return p; }

    operator T*() const { return p; }

    array_ptr(){}
    //array_ptr(std::nullptr_t) : p(nullptr), i(0){}
    array_ptr(T* ptr, int _i DEFAULT_DATA) : p(ptr), i(_i){}

};

template<>
struct array_ptr<void> {
    using T = void;
    T* p;
    int i; //some additional information

//    T& operator*() const { return *p; }
    T* operator->() const { return p; }

    operator T*() const { return p; }
    template<class T>
    operator array_ptr<T>() const{return array_ptr<T>((T*)p, i);}
//    array_ptr& operator++(){++p; return *this;}
    array_ptr(){}
    array_ptr(std::nullptr_t) : p(nullptr){}
    array_ptr(T* ptr, int _i DEFAULT_DATA) : p(ptr), i(_i){}
    template<class Other>
    array_ptr(array_ptr<Other> other) : p(other.p), i(other.i){}
};

template<>
struct array_ptr<void const> {
    using T = void const;
    T* p;
    int i; //some additional information

//    T& operator*() const { return *p; }
    T* operator->() const { return p; }

    operator T*() const { return p; }
//    array_ptr& operator++(){++p; return *this;}
//  template<class Other> array_ptr(array_ptr<Other> const& other) : p(other.p), i(other.i){}
    array_ptr(){}
    array_ptr(std::nullptr_t) : p(nullptr){}
    array_ptr(T* ptr, int _i DEFAULT_DATA) : p(ptr), i(_i){}
    template<class Other>
    array_ptr(array_ptr<Other> other) : p(other.p), i(other.i){}
};

struct some_managed_shared_memory {
    array_ptr<void> allocate(size_t n) { return array_ptr<void>(::malloc(n), 99); }
    void  deallocate(array_ptr<void> ptr) { if (ptr) ::free(ptr.p); }
};

template<typename T>
struct allocator{
    using version = boost::container::version_1;

    using value_type = T;
    using pointer = array_ptr<T>; // simulates T*
    using const_pointer = array_ptr<T const>; // simulates T const*
    using void_pointer = array_ptr<void>; // simulates void*
    using const_void_pointer = array_ptr<void const>; // simulates void const*

    some_managed_shared_memory& msm_;
    allocator(some_managed_shared_memory& msm) : msm_(msm){}
    array_ptr<T> allocate(size_t n){
        auto ret = msm_.allocate(n*sizeof(T));
        return static_cast<array_ptr<T>>(ret);
    }
    void deallocate(array_ptr<T> ptr, std::size_t = 0){
        msm_.deallocate(ptr);
    }
};

#ifdef DEFINE_ARITHMETIC_OPERATIONS
    template <typename T, typename N>
    array_ptr<T> operator+(array_ptr<T> const& p, N n) { return array_ptr<T>(p.p+n, p.i); }

    template <typename T>
    array_ptr<T>& operator++(array_ptr<T>& p) { return ++p.p, p; }

    template <typename T>
    array_ptr<T> operator++(array_ptr<T>& p, int) { auto q = p.p++; return array_ptr<T>(q, p.i); }

    template <typename T, typename N>
    array_ptr<T> operator-(array_ptr<T> const& p, N n) { return array_ptr<T>(p.p-n, p.i); }

    template <typename T>
    ptrdiff_t operator-(array_ptr<T> const& a, array_ptr<T> const& b) { return a.p - b.p; }
#endif


int main() {
    std::cout << boost::container::container_detail::impl::version<allocator<int> >::value << "\n";

    if (1) { // some diagnostics
        array_ptr<int> p;
        auto rawp = boost::container::container_detail::to_raw_pointer(p);
        std::cout << typeid(rawp).name() << "\n";

        std::cout << typeid(p).name() << "\n";
        std::cout << typeid(p + 5).name() << "\n";
        std::cout << typeid(p - 5).name() << "\n";
    }

    some_managed_shared_memory realm;
    boost::container::vector<int, allocator<int> > v(10, realm);
    assert( v[4] == 0 );
    v[4] = 1;
    assert( v[4] == 1 );
    for(std::size_t i = 0; i != v.size(); ++i) std::cout << v[i] << std::endl;

    // these compile:
    v.push_back(12);
    v.resize(100);
    v.assign({1,2,3,4,5});
}

Отпечатки

1
Pi
9array_ptrIiE
9array_ptrIiE
9array_ptrIiE
0
0
0
0
1
0
0
0
0
0
person sehe    schedule 07.07.2017
comment
да, наличие оператора преобразования заставляет явно определять арифметические операторы, иначе там будет потеря информации. Я думаю, что исправил эту проблему, однако проблема сохраняется, все пути в функции resize выполняют преобразование в необработанный указатель T*, а затем обратно в pointer. Даже в вашем коде, если вы добавите проверку при освобождении void deallocate(array_ptr<void> ptr) { if (ptr){ assert(ptr.i == 99); ::free(ptr.p); } } , вы увидите эту информацию, если она потеряна (после изменения размера). - person alfC; 07.07.2017
comment
Я думаю, нам нужен какой-то трюк, используемый в interprocess::offset_ptr boost .org/doc/libs/1_64_0/doc/html/interprocess/offset_ptr.html. В случае offset_ptr кажется, что информация offset помещается в сам указатель, а затем реконструируется в присваивании (в настоящее время не в нашем коде). Я пытался сделать это, но не без ошибки сегментации. Я не знаю, почему они сделали код изменения размера таким сложным и уродливым. Я думаю, что они пытались оптимизировать код, чтобы использовать некоторое перемещение памяти, но в процессе они сделали контейнер менее универсальным. - person alfC; 07.07.2017
comment
О проигрыше знаю, если ptr.i - указал в ответе. Я предположил, что offset_ptr оставит указатель непреобразованным, так что segment_manager сможет выполнить преобразование. Я не искал логику изменения размера, чтобы узнать, была ли она излишне сложной (интересно, я посмотрю позже). Моя интуиция подсказывает, что для правильной семантики безопасности исключений требуется внутренняя сложность. Контейнеры стандартных библиотек обманчиво просты. - person sehe; 07.07.2017
comment
Связано: формальная спецификация проблемы и пути вперед quuxplusone.github.io/draft/ причудливые указатели.html - person alfC; 23.09.2017