More issues with pool allocator with free list

0

In std::map, this ends up causing an error when the first object is constructed. I've checked the debugger, and I see that free_list::init() creates the consecutive memory addresses correctly. I'm aware this allocator cannot be used in vector or other related containers, but it's only meant to work with the nodular containers.

I get a run-time error from this in xutility (in VC12), at line 158:

_Container_proxy *_Parent_proxy = _Parent->_Myproxy;

Checking the debugger, it appears that _Parent was never initialized, bringing about the 0xC0000005 run-time error. Why or how it didn't get initialized and why this occurred when the first object was being constructed (after std::map did 3 separate allocations), I do not know.

I would like to have this work with std::map and std::list and the other nodular containers and am not worried about whether it can perform in std::vector, etc.

#include <algorithm>

class free_list {
public:
    free_list() {}

    free_list(free_list&& other)
        : m_next(other.m_next) {
        other.m_next = nullptr;
    }

    free_list(void* data, std::size_t num_elements, std::size_t element_size) {
        init(data, num_elements, element_size);
    }

    free_list& operator=(free_list&& other) {
        m_next = other.m_next;
        other.m_next = nullptr;
    }

    void init(void* data, std::size_t num_elements, std::size_t element_size) {
        union building {
            void* as_void;
            char* as_char;
            free_list* as_self;
        };

        building b;

        b.as_void = data;
        m_next = b.as_self;
        b.as_char += element_size;

        free_list* runner = m_next;
        for (std::size_t s = 1; s < num_elements; ++s) {
            runner->m_next = b.as_self;
            runner = runner->m_next;
            b.as_char += element_size;
        }
        runner->m_next = nullptr;
    }

    free_list* obtain() {
        if (m_next == nullptr) {
            return nullptr;
        }
        free_list* head = m_next;
        m_next = head->m_next;
        return head;
    }

    void give_back(free_list* ptr) {
        ptr->m_next = m_next;
        m_next = ptr;
    }

    free_list* m_next;

};

template<class T>
class pool_alloc {
    typedef pool_alloc<T> myt;

public:
    typedef std::size_t size_type;
    typedef std::ptrdiff_t difference_type;
    typedef T value_type;
    typedef T& reference;
    typedef const T& const_reference;
    typedef T* pointer;
    typedef const T* const_pointer;

    typedef std::false_type propagate_on_container_copy_assignment;
    typedef std::true_type propagate_on_container_move_assignment;
    typedef std::true_type propagate_on_container_swap;

    template<class U> struct rebind {
        typedef pool_alloc<U> other;
    };

    ~pool_alloc() {
        destroy();
    }
    pool_alloc() : data(nullptr), fl(), capacity(4096) {
    }
    pool_alloc(size_type capacity) : data(nullptr), fl(), capacity(capacity) {}

    pool_alloc(const myt& other)
        : data(nullptr), fl(), capacity(other.capacity) {}

    pool_alloc(myt&& other)
        : data(other.data), fl(std::move(other.fl)), capacity(other.capacity) {
        other.data = nullptr;
    }

    template<class U>
    pool_alloc(const pool_alloc<U>& other) 
        : data(nullptr), fl(), capacity(other.max_size()) {}

    myt& operator=(const myt& other) {
        destroy();
        capacity = other.capacity;
    }

    myt& operator=(myt&& other) {
        destroy();
        data = other.data;
        other.data = nullptr;
        capacity = other.capacity;
        fl = std::move(other.fl);
    }

    static pointer address(reference ref) {
        return &ref;
    }

    static const_pointer address(const_reference ref) {
        return &ref;
    }

    size_type max_size() const {
        return capacity;
    }

    pointer allocate(size_type) {
        if (data == nullptr) create();
        return reinterpret_cast<pointer>(fl.obtain());
    }

    void deallocate(pointer ptr, size_type) {
        fl.give_back(reinterpret_cast<free_list*>(ptr));
    }

    template<class... Args>
    static void construct(pointer ptr, Args&&... args) {
        ::new (ptr) T(std::forward<Args>(args)...);
    }

    static void destroy(pointer ptr) {
        ptr->~T();
    }

    bool operator==(const myt& other) const {
        return reinterpret_cast<char*>(data) ==
            reinterpret_cast<char*>(other.data);
    }

    bool operator!=(const myt& other) const {
        return !operator==(other);
    }

private:

    void create() {
        data = ::operator new(capacity * sizeof(value_type));
        fl.init(data, capacity, sizeof(value_type));
    }

    void destroy() {
        ::operator delete(data);
        data = nullptr;
    }

    void* data;
    free_list fl;
    size_type capacity;

};

template<>
class pool_alloc < void > {
public:
    template <class U> struct rebind { typedef pool_alloc<U> other; };

    typedef void* pointer;
    typedef const void* const_pointer;
    typedef void value_type;
};

The problem comes when std::pair is being constructed (in MSVC12 utility at line 214):

    template<class _Other1,
        class _Other2,
        class = typename enable_if<is_convertible<_Other1, _Ty1>::value
            && is_convertible<_Other2, _Ty2>::value,
            void>::type>
        pair(_Other1&& _Val1, _Other2&& _Val2)
            _NOEXCEPT_OP((is_nothrow_constructible<_Ty1, _Other1&&>::value
                && is_nothrow_constructible<_Ty2, _Other2&&>::value))
        : first(_STD forward<_Other1>(_Val1)),
                second(_STD forward<_Other2>(_Val2))
        {   // construct from moved values
        }

Even after stepping in, the run-time error occurs, the same as described above with _Parent not being initialized.

c++11
memory-management
initialization
asked on Stack Overflow Jun 25, 2014 by beneficii • edited Apr 28, 2019 by Cœur

1 Answer

0

I was able to answer my own question through extensive debugging. Apparently, VC12's std::map implementation at least at times will cast an _Alnod (permanent allocator that stays in scope for the life of the map, which is used to allocate and deallocate the nodes in the map, what I'd expect to be what actually calls allocate() and deallocate()) as an _Alproxy, a temporary allocator which creates some sort of object called _Mproxy (or something like that) using allocate(). The problem, though, is that VC12's implementation then lets _Alproxy go out of scope while still expecting the pointer to the allocated object to remain valid, so it is clear then that I would have to use ::operator new and ::operator delete on an object like _Mproxy: using a memory pool that then goes out of scope while a pointer to a location in it remains is what causes the crash.

I came up with what I suppose could be called a dirty trick, a test that is performed when copy-constructing or copy-assigning an allocator to another allocator type:

template<class U>
pool_alloc(const pool_alloc<U>& other)
    : data(nullptr), fl(), capacity(other.max_size()), use_data(true) {
    if (sizeof(T) < sizeof(U)) use_data = false;
}

I added the bool member use_data to the allocator class, which if true means to use the memory pool and which if false means to use ::operator new and ::operator delete. By default, it is true. The question of its value arises when the allocator gets cast as another allocator type whose template parameter's size is smaller than that of the source allocator; in that case, use_data is set to false. Because this _Mproxy object or whatever it's called is rather small, this fix seems to work, even when using std::set with char as the element type.

I've tested this using std::set with type char in both VC12 and GCC 4.8.1 in 32-bit and have found that in both cases it works. When allocating and deallocating the nodes in both cases, the memory pool is used.

Here is the full source code:

#include <algorithm>

class free_list {
public:

    free_list() {}

    free_list(free_list&& other)
        : m_next(other.m_next) {
        other.m_next = nullptr;
    }

    free_list(void* data, std::size_t num_elements, std::size_t element_size) {
        init(data, num_elements, element_size);
    }

    free_list& operator=(free_list&& other) {
        if (this != &other) {
            m_next = other.m_next;
            other.m_next = nullptr;
        }
        return *this;
    }

    void init(void* data, std::size_t num_elements, std::size_t element_size) {
        union building {
            void* as_void;
            char* as_char;
            free_list* as_self;
        };

        building b;

        b.as_void = data;
        m_next = b.as_self;
        b.as_char += element_size;

        free_list* runner = m_next;
        for (std::size_t s = 1; s < num_elements; ++s) {
            runner->m_next = b.as_self;
            runner = runner->m_next;
            b.as_char += element_size;
        }
        runner->m_next = nullptr;
    }

    free_list* obtain() {
        if (m_next == nullptr) {
            return nullptr;
        }
        free_list* head = m_next;
        m_next = head->m_next;
        return head;
    }

    void give_back(free_list* ptr) {
        ptr->m_next = m_next;
        m_next = ptr;
    }

    free_list* m_next;

};

template<class T>
class pool_alloc {
    typedef pool_alloc<T> myt;

public:
    typedef std::size_t size_type;
    typedef std::ptrdiff_t difference_type;
    typedef T value_type;
    typedef T& reference;
    typedef const T& const_reference;
    typedef T* pointer;
    typedef const T* const_pointer;

    typedef std::false_type propagate_on_container_copy_assignment;
    typedef std::true_type propagate_on_container_move_assignment;
    typedef std::true_type propagate_on_container_swap;

    myt select_on_container_copy_construction() const {
        return *this;
    }

    template<class U> struct rebind {
        typedef pool_alloc<U> other;
    };

    ~pool_alloc() {
        clear();
    }
    pool_alloc() : data(nullptr), fl(), capacity(4096), use_data(true) {}

    pool_alloc(size_type capacity) : data(nullptr), fl(), 
        capacity(capacity), use_data(true) {}

    pool_alloc(const myt& other)
        : data(nullptr), fl(), capacity(other.capacity), 
            use_data(other.use_data) {}

    pool_alloc(myt&& other)
        : data(other.data), fl(std::move(other.fl)), capacity(other.capacity),
            use_data(other.use_data) {
        other.data = nullptr;
    }

    template<class U>
    pool_alloc(const pool_alloc<U>& other) 
        : data(nullptr), fl(), capacity(other.max_size()), use_data(true) {
        if (sizeof(T) < sizeof(U)) use_data = false;
    }


    myt& operator=(const myt& other) {
        if (*this != other) {
            clear();
            capacity = other.capacity;
            use_data = other.use_data;
        }
    }

    myt& operator=(myt&& other) {
        if (*this != other) {
            clear();
            data = other.data;
            other.data = nullptr;
            capacity = other.capacity;
            use_data = other.use_data;
            fl = std::move(other.fl);
        }
        return *this;
    }

    template<class U>
    myt& operator=(const pool_alloc<U>& other) {
        if (this != reinterpret_cast<myt*>(&other)) {
            capacity = other.max_size();
            if (sizeof(T) < sizeof(U))
                use_data = false;
            else
                use_data = true;
        }
        return *this;
    }

    static pointer address(reference ref) {
        return &ref;
    }

    static const_pointer address(const_reference ref) {
        return &ref;
    }

    size_type max_size() const {
        return capacity;
    }

    pointer allocate(size_type) {
        if (use_data) {
            if (data == nullptr) create();
            return reinterpret_cast<pointer>(fl.obtain());
        } else {
            return reinterpret_cast<pointer>(::operator new(sizeof(T)));
        }
    }

    void deallocate(pointer ptr, size_type) {
        if (use_data) {
            fl.give_back(reinterpret_cast<free_list*>(ptr));
        } else {
            ::operator delete(reinterpret_cast<void*>(ptr));
        }
    }

    template<class... Args>
    static void construct(pointer ptr, Args&&... args) {
        ::new ((void*)ptr) value_type(std::forward<Args>(args)...);
    }

    static void destroy(pointer ptr) {
        ptr->~value_type();
    }

    bool operator==(const myt& other) const {
        return reinterpret_cast<char*>(data) ==
            reinterpret_cast<char*>(other.data);
    }

    bool operator!=(const myt& other) const {
        return !operator==(other);
    }

private:

    void create() {
        size_type size = sizeof(value_type) < sizeof(free_list*) ? 
            sizeof(free_list*) : sizeof(value_type);
        data = ::operator new(capacity * size);
        fl.init(data, capacity, size);
    }

    void clear() {
        ::operator delete(data);
        data = nullptr;
    }

    void* data;
    free_list fl;
    size_type capacity;
    bool use_data;
};

template<>
class pool_alloc < void > {
public:
    template <class U> struct rebind { typedef pool_alloc<U> other; };

    typedef void* pointer;
    typedef const void* const_pointer;
    typedef void value_type;
};

template<class Container, class Alloc>
void change_capacity(Container& c, typename Alloc::size_type new_capacity) {
    Container temp(c, Alloc(new_capacity));
    c = std::move(temp);
}

Since the allocator is not automatic-growing (don't know how to make such a thing), I have added the change_capacity() function.

answered on Stack Overflow Jun 26, 2014 by beneficii • edited Jun 26, 2014 by beneficii

User contributions licensed under CC BY-SA 3.0