Может ли замена std::allocator позволить std::set работать с void* и отдельными функциями копирования/деаллока?

Я борюсь с проблемой, когда какой-то устаревший код пытается общаться с современными системами. В частности, C++11 версии STL (бонусные баллы, если предлагаемое вами решение работает с C++03, отрицательные баллы, если решение работает только с C++17, но меня все еще интересует случае я могу использовать это как аргумент для обновления).

Моей функции передается массив void* и трех указателей на функции, которые являются функциями для сравнения, копирования и освобождения данных в каждом указателе void (все указатели void имеют один и тот же тип данных для любого заданного вызова). Короче говоря, у меня есть все необходимое для того, чтобы эти void* выглядели как объекты, но на самом деле они не являются объектами.

В моей функции я хотел бы использовать некоторые библиотеки для std::set и std::map с этими данными (также некоторые другие библиотеки в нашей собственной кодовой базе, но set и map — хорошие отправные точки). С void* нужно обращаться как с объектами-значениями, т. е. когда я делаю mySet.insert(x), это должно выделять новый указатель (у меня есть способ запросить размер указателя), а затем вызывать функцию копирования чтобы скопировать содержимое x в набор.

TL;DR: Кто-нибудь знает, как написать собственный распределитель, который будет работать с std::set для типа, чьи инструкции копирования/удаления не включены в конструктор копирования?

---------- конец вопроса (остальное я уже пробовал) ---------

Очевидно, я мог бы объединить четыре вещи в класс:

class BundleStuffTogether {
    BundleStuffTogether(void *data, CompareFunc compare, CopyFunc copy, DeallocFunc dealloc);
    // And create the rest of class accordingly, storing the values, 
    // and with destructor calling dealloc, copy constructor calling
    // copy, etc.
};

Я хотел бы избежать выделения этого класса, если смогу. Мне не нужны накладные расходы памяти для каждой записи в наборе, требующем 4-кратного размера (многие указатели относятся к небольшим объемам данных, поэтому относительный размер велик).

Я рассматриваю просто использование std::set и творческое заполнение этих двух пробелов. Я могу передать указатель функции сравнения в качестве объекта сравнения для набора и карты. Это просто. Более сложная часть — это выделение, освобождение и копирование.

Я пытался написать собственный аллокатор, и он действительно заработал, когда я вызвал его непосредственно в тестах, но когда я подключил его к std::set, все развалилось. Я создал класс только для того, чтобы удерживать void*, чтобы иметь возможность специализации шаблона распределителя (подробности ниже).

Мой первый трюк состоял в том, чтобы создать этот класс:

class Wrapper {
     public: void *_ptr;
};
// which allows for this, given "void *y":
Wrapper *wrap = static_cast<Wrapper*>(&y);

Это дает мне определенный тип, который я могу использовать для специализации шаблона. Используя это, я попытался создать успешную специализацию типа std::allocator для Wrapper. Это работало для Wrapper само по себе, но развалилось, когда я попытался добавить дополнительные поля пользовательской специализации для хранения функций — распределители должны сравниваться как равные.

Затем я клонировал std::allocator и создал свой собственный класс шаблона MyAllocator — точно такой же код, как у std::allocator, — а затем создал специализацию шаблона для Wrapper. Затем я дал ОБА основному шаблону и моей специализации функции, необходимые для управления пустотой*, так что теперь они сравниваются как равные.

И это было успешным в качестве распределителя! Я протестировал несколько вариантов, используя аллокаторы напрямую, и это сработало. Но он развалился, когда я подключил его к std::set. Набор не выделяет мой класс напрямую. Он выделяет узлы, которые содержат мой класс... и узел предполагает, что у моего класса есть конструктор копирования. вздох Я думал, что контракт с std::set заключается в том, что он будет использовать метод "construct" для фактического создания объекта Wrapper в своем собственном узле, но, по-видимому, это не так.

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

Кто-нибудь может предложить решение, отличное от решения BundleStuffTogether, которое было у меня в начале? Моя следующая лучшая идея — выделить std::set и переписать его внутренности, и я действительно не хочу идти по этому пути, если смогу этого избежать.


person srm    schedule 11.04.2018    source источник
comment
Я считаю, что это хороший вопрос, но здесь много текста/информации. Если бы вы могли сильно обрезать его, было бы легче понять и ответить   -  person Justin    schedule 11.04.2018
comment
Я хотел бы избежать выделения этого класса, если смогу. Это преждевременная оптимизация. Я не согласен с тем, что класс вводит накладные расходы на память.   -  person Cheers and hth. - Alf    schedule 11.04.2018
comment
@ Джастин Я определил проблему с самого начала. Хвост просто прикрывает то, что я уже пытался избежать перефразирования этих идей.   -  person srm    schedule 11.04.2018
comment
@srm Да. Мое предложение состоит в том, чтобы обрезать обе части. Обычно читателям намного легче понять, что вы говорите, если вы сделаете свои вопросы более краткими, чем здесь.   -  person Justin    schedule 11.04.2018
comment
@Cheersandhth.-Альф Это не преждевременно. Дополнительные распределения были постоянной проблемой с этим набором данных. И я все равно был бы признателен за ответ на вопрос в любом случае.   -  person srm    schedule 11.04.2018
comment
Я согласен с @Justin, как объяснено где-то в разделе Как спросить, вы должны написать свой вопрос, как если бы вы спрашивали занятой коллега. И поверьте мне, этот занятой коллега не даст вам закончить половину первой четверти начала вашего вопроса.   -  person YSC    schedule 11.04.2018
comment
С void* нужно обращаться как с объектами-значениями, т. е. когда я делаю mySet.insert(x), это должно выделять новый указатель: неясно! Вы хотите выделить новый указатель или новый указатель? Если последнее, как инициализировать новое выделенное хранилище из указателя? С функцией копирования, да?   -  person YSC    schedule 11.04.2018
comment
Особенно при работе с void* будьте очень осторожны в отношении разницы между указателем и на что он указывает. Например, вы, конечно же, не хотите выделять новый указатель и копировать в него данные. Вы хотите выделить память и скопировать в нее данные.   -  person Pete Becker    schedule 12.04.2018
comment
@YSC Учитывая void *X, выделите новый блок памяти, затем скопируйте содержимое, на которое указывает X, в новый блок.   -  person srm    schedule 12.04.2018
comment
Похоже на проблему XY. Как использовать распределитель предполагает, что проблема заключается в распределителе.   -  person MSalters    schedule 12.04.2018


Ответы (2)


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

struct WrapperType {
  using compare_t = int(void*,void*);
    using copy_t    = void*(void*);
    using free_t    = void(void*);

    compare_t* _compare;
    copy_t*    _copy;
    free_t*    _free;
};
struct Wrapper {
    void*      _data;
    WrapperType* _type;

    explicit Wrapper(void* data, WrapperType* type) noexcept : _data(data), _type(type) {}
    Wrapper(Wrapper&& other) noexcept : _data(other._data), _type(other._type) {}
    Wrapper& operator=(Wrapper&& other) noexcept 
    {reset(); _data=other.release(); _type=other._type; return *this;}
    ~Wrapper() noexcept {reset();}
    void reset() noexcept {_type._free(_data); _data=nullptr;}
    void* release() noexcept {void* data=_data; _data=nullptr; return data;}
    boolean operator<(const Wrapper&other) noexcept {
        assert(_type==other._type);
        return _type._compare(_data, other._data)<0;
    }
};

noexcept очень полезен здесь, в конструкторе перемещения и операторах присваивания перемещения. С ними библиотека C++ обычно будет использовать их. В противном случае библиотека C++ обычно предпочитает копии.

person Mooing Duck    schedule 11.04.2018
comment
Это определенно улучшение по сравнению с тем, на что я смотрел. Спасибо. - person srm; 12.04.2018

Во-первых, лично я избегал настраиваемых распределителей, так как они мне не кажутся правильными. Это не очень хорошая причина, и вы можете получить ответ на свой вопрос на основе распределителя, но не от меня. (См. доклад Андрея Александреску по этому вопросу на CppCon 2015: std::allocator предназначен для выделения того, что std: :vector — к досаде).

Во-вторых, если вы беспокоитесь о накладных расходах памяти, вам действительно не следует использовать std::set, у которого много накладных расходов памяти. Это также довольно медленно.

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

  1. Имейте статические переменные для трех указателей и обязательно установите их перед использованием указателей. Это должно работать, если вы можете гарантировать, что никогда не будете использовать такие объекты с разными функциями одновременно.
  2. Пусть каждый экземпляр содержит ссылку (= один указатель) на другой класс, где этот другой класс содержит 3 указателя функций. Вам нужно будет управлять временем жизни этого единственного общего экземпляра класса, содержащего 3 указателя, но это не должно быть слишком плохо.
person einpoklum    schedule 11.04.2018