Доступ к хранилищу через reinterpret_cast и std :: launder - PullRequest
0 голосов
/ 22 апреля 2020

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

template<class T>
class ObjectPool {
public:
    ObjectPool( std::size_t cap );
    virtual ~ObjectPool();
    void* allocate() noexcept(false);
    void deallocate( void* ptr );
private:
    template<typename M>
    static constexpr const M& max( const M& a, const M& b ) {
        return a < b ? b : a;
    }
    using storage = typename std::aligned_storage<max(sizeof(T), sizeof(T*)), std::alignment_of<T>::value>::type;
    storage* pool;
    std::mutex mutex;
    std::size_t capacity;
    std::size_t counter;
    T* deletedItem;
};

template<class T>
ObjectPool<T>::ObjectPool( std::size_t cap ) :
        capacity(cap), counter(0), deletedItem(nullptr) {
    if ( capacity > 0 )
        pool = ::new storage[cap];
    else
        pool = nullptr;
}

template<class T>
ObjectPool<T>::~ObjectPool() {
    ::delete[] pool;
}

template<class T>
void* ObjectPool<T>::allocate() noexcept(false) {
    std::lock_guard<std::mutex> l(mutex);
    if ( deletedItem ) {
        T* result = deletedItem;
        deletedItem = *(reinterpret_cast<T**>(deletedItem)); //<-----undefined behavior??
        return result;
    }

    if ( counter >= capacity ) {
        throw std::bad_alloc();
    }

    return std::addressof(pool[counter++]);
}

template<class T>
void ObjectPool<T>::deallocate( void* ptr ) {
    std::lock_guard<std::mutex> l(mutex);
    *(reinterpret_cast<T**>(ptr)) = deletedItem;  //<-----undefined behavior??
    deletedItem = static_cast<T*>(ptr);
}

Я пытаюсь чтобы понять, имеет ли этот класс неопределенное поведение в соответствии со стандартом C ++ 17. Нужно ли использовать std::launder? Насколько я понимаю, это не так, поскольку задействованы только пустые указатели и T указатели. Кроме того, когда в deallocate объект уже был уничтожен, значит, он должен быть в безопасности.

1 Ответ

3 голосов
/ 22 апреля 2020

Ваш код показывает UB по каждому стандарту C ++, даже C ++ 20, который позволяет (при определенных обстоятельствах) неявное создание объекта. Но не по причинам, связанным с launder.

. Вы не можете просто взять кусок памяти, притвориться, что там есть T*, и обращаться с ним как с существующим. Да, даже для фундаментальных типов, таких как указатели. Вы должны создать объектов, прежде чем использовать их. Так что, если у вас есть неиспользуемая часть памяти, и вы хотите вставить в нее T*, вам нужно создать ее с помощью Placement-New.

Итак, давайте перепишем ваш код (обратите внимание, что это компилируется, но в противном случае не проверено; главное, что вы должны создать элементы вашего связанного списка):

template<class T>
class ObjectPool {
public:
    ObjectPool( std::size_t cap );
    virtual ~ObjectPool();
    void* allocate() noexcept(false);
    void deallocate( void* ptr );
private:

    using empty_data = void*; //The data stored by an empty block. Does not point to a `T`.
    using empty_ptr = empty_data*; //A pointer to an empty block.

    static constexpr size_t entry_size = std::max(sizeof(T), sizeof(empty_data));
    static constexpr std::align_val_t entry_align =
      std::align_val_t(std::max(alignof(T), alignof(empty_data))); //Ensure proper alignment

    void* pool;
    std::mutex mutex;
    std::size_t capacity;
    //std::size_t counter;  //We don't need a counter; the pool is empty if `freeItem` is NULL
    empty_ptr freeItem; //Points to the first free item.
};

template<class T>
ObjectPool<T>::ObjectPool( std::size_t cap ) :
    pool(::operator new(entry_size * cap, entry_align)),
    capacity(cap)
{
    //Build linked-list of free items, from back to front.
    empty_data previous = nullptr; //Last entry points to nothing.
    std::byte *byte_pool = reinterpret_cast<std::byte*>(pool); //Indexable pointer into memory
    auto curr_ptr = &byte_pool[entry_size * capacity]; //Pointer to past-the-end element.
    do
    {
        curr_ptr -= entry_size;

        //Must *create* an `empty_data` in the storage.
        empty_ptr curr = new(curr_ptr) empty_data(previous);
        previous = empty_data(curr); //`previous` now points to the newly-created `empty_data`.
    }
    while(curr_ptr != byte_pool);

    freeItem = empty_ptr(previous);
}

template<class T>
ObjectPool<T>::~ObjectPool()
{
    ::operator delete(pool, entry_size * capacity, entry_align);
}

template<class T>
void* ObjectPool<T>::allocate() noexcept(false) {
    std::lock_guard<std::mutex> l(mutex);
    if(!freeItem) { throw std::bad_alloc(); } //No free item means capacity full.

    auto allocated = freeItem;
    freeItem = empty_ptr(*freeItem); //Next entry in linked list is free or nullptr.
    return allocated;
}

template<class T>
void ObjectPool<T>::deallocate( void* ptr ) {
    std::lock_guard<std::mutex> l(mutex);

    //Must *create* an `empty_data` in the storage. It points to the current free item.
    auto newFree = new(ptr) empty_data(freeItem);
    freeItem = newFree;
}

...