Как использовать STL-совместимые распределители для распределения гетерогенных памяти

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

template<class T>
class Buf
{
    size_t n;
    int refs;
    explicit Buf(size_t n) : n(n) { }
    // other declarations are here as appropriate

    // Followed in memory by:
    // T items[n];
};

Это было бы легко с operator new:

template<class T>
Buf<T> *make_buf(size_t n)
{
    // Assume the caller will take care of constructing the array elements
    return new(operator new(sizeof(Buf<T>) + sizeof(T) * n)) Buf<T>(n);
}

template<class T>
void free_buf(Buf<T> *p)
{
    // Assume the caller has taken care of destroying the array elements
    p->~Buf<T>();
    return operator delete(p);
}

template<class T>
T *get_buf_array(Buf<T> *p)
{
    return reinterpret_cast<T *>(reinterpret_cast<char *>(p) + sizeof(Buf<T>));
}

Но теперь, как это реализовать с помощью стандартного распределителя SomeAllocator?

Гарантируется ли, что SomeAllocator::rebind<char>::other::allocate вернет память, подходящую для любого типа объекта? Если да, могу ли я в противном случае просто использовать распределитель какого-либо типа char? Если нет, есть ли у меня какие-либо альтернативы или эта задача невозможна для распределителей в целом? (В худшем случае, я полагаю, я мог бы наложить указатели на uintptr_t и выровнять их вручную, но мне интересно, есть ли лучший способ.)

Ответ 1

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

+-----------+
|Buf<T>     |
+-------+---+---+-------+-------+
|T[0]   | T[1]  |T[2]   |  T[3]...
+-------+-------+-------+-------+

With the non-overlapping T[2], T[3], ... being the required array of T.

template<class T>
class Buf
{
    size_t n;
    int refs;
    explicit Buf(size_t n) : n(n) { }
    // other declarations are here as appropriate

    // Followed in memory by:
    // T items[n];
};

Количество разрушенных элементов будет: -

const size_t lead = ( sizeof(Buf<T>) + sizeof(T) - 1) / sizeof(T);

Наконец, к исходной памяти я можно получить доступ к

(reinterpret_cast<T*>( this ) )[ i + lead ];

Ответ 2

Я боюсь, что вы делаете необоснованные предположения о том, что требует стандарт С++. То, что вы пытаетесь сделать, возможно, не будет в целом возможным.

Распределитель по умолчанию (new или malloc) требуется для возврата указателя на блок памяти, который соответствующим образом выровнен для any complete object type with a fundamental alignment requirement. Размер должен быть at least as large as the requested size. У пользовательских распределителей разные требования, в зависимости от того, что они выделяют. Распределитель для одного типа не гарантированно возвращает хранилище, подходящее для другого. Конечно, если вы реализуете собственный распределитель, вы можете убедиться, что он возвращает то, что вам нужно.

Компилятор должен удовлетворять некоторым ограничениям на макет памяти, но он не гарантирует, что что-то помещено в память сразу после чего-то другого. В зависимости от требований к выравниванию может быть добавлена ​​вставка байтов.

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

Ответ 3

Я думаю, что стандартный совместимый распределитель, который вам подходит? Поскольку вам не требуется, чтобы ваш распределитель использовался со структурой данных stl, на самом деле не обязательно удовлетворять его требованиям, даже если вы также можете это сделать, поскольку я думаю, что это аккуратный способ сделать это, и в этом случае вы можете реализовать буфер с помощью std::vector с вашим специализированным распределителем стиля stl в качестве параметра шаблона. Что касается гарантий выравнивания оператора new и оператора new [], я предлагаю вам взглянуть на:

Есть ли какая-либо гарантия выравнивания возврата адреса с помощью новой операции С++?.

Если ваши проблемы с выравниванием для примитивных типов, таких как парные и т.д., вы в значительной степени покрыты std:: align, как вы можете видеть в http://en.cppreference.com/w/cpp/memory/align.

Однако, если у вас есть более строгие требования к выравниванию, такие как выравнивание каждого элемента к кеш-строкам и т.д., или T - тип с размером, где sizeof (T) mod alignment!= 0, могут возникнуть проблемы при распределении массивов T. В таких случаях, даже если первый элемент массива выровнен для соответствия требованию, это не означает, что все последующие элементы также будут выровнены.

Ответ 4

Ограничьте выравнивание, переставив распределитель на специализацию std::aligned_storage.

typedef std::aligned_storage_t< 1, std::max( alignof (Buf<T>), alignof (T) ) >
        unit_type; // lowest-common-denominator type for aligned allocation

std::size_t unit_count // number of unit_type array elements needed
    = ( sizeof (Buf<T>) + sizeof (T) * n // = actual used storage
           + sizeof (unit_type) - 1 )    //   + alignment padding
      / sizeof (unit_type);              // divided by element size

typedef typename std::allocator_traits< SomeAllocator >::rebind_alloc< unit_type >
        rebound;

rebound a( alloc_parm ); // Retain this somewhere if statefulness is allowed.
ptr = std::allocator_traits< rebound >::allocate( a, unit_count );

(Помните, что доступ всех распределителей проходит через allocator_traits!)