Search code examples
c++c++11spliceallocator

Why is there an error message when splicing list elements from lists with different allocators? And how can this be fixed?


Hello I'm trying to transfer/move an element from one list (in the example foo) at (end()-1) to another list (in the example called bar) to position begin().

The only problem is that one of the lists is using an custom made allocator. Which probably results to the following error message:

../src/TestAllocator.cpp:120:28: error: 
no matching function for call to
‘std::list<int>::splice  (  std::_List_iterator<int>&, 
                            std::list<int, CustomAllocator<int> >&, 
                            std::_List_iterator<int>&)’

My Question here is:

  • Why is there an error message when splicing list elements from lists with different allocators?

  • And how can this be fixed?

Code on Coliru

#include <limits>   // numeric_limits
#include <iostream>
#include <typeinfo> // typeid

// container
#include <vector>
#include <list>
#include <forward_list>

/// state stored as static member(s) of an auxiliary(=hilfs/zusatz) class
struct CustomAllocatorState {
    static std::size_t m_totalMemAllocated;
};
std::size_t CustomAllocatorState::m_totalMemAllocated = 0;

/// @brief  The @a custom allocator
/// @tparam  T  Type of allocated object
template<typename T>
class CustomAllocator {
public:
    // type definitions
    typedef std::size_t     size_type;          /** Quantities of elements */
    typedef std::ptrdiff_t  difference_type;    /** Difference between two pointers */
    typedef T*              pointer;            /** Pointer to element */
    typedef const T*        const_pointer;      /** Pointer to constant element */
    typedef T&              reference;          /** Reference to element */
    typedef const T&        const_reference;    /** Reference to constant element */
    typedef T               value_type;         /** Element type */

    template<typename U>
    struct rebind {
        typedef CustomAllocator<U> other;
    };


    CustomAllocator() throw() {
        std::cout << "construct " << typeid(T).name() << std::endl;
    }

    CustomAllocator(const CustomAllocator&) throw() {
        std::cout << "copy construct " << typeid(T).name() << std::endl;
    }

    template<class U>
    CustomAllocator() throw() {
    }

    ~CustomAllocator() throw() {}


    // allocate but don't initialize num elements of type T
    pointer allocate(size_type num, const void* = 0) {
        CustomAllocatorState::m_totalMemAllocated += num * sizeof(T);
        // print message and allocate memory with global new
        std::cout << "allocate " << num << " element(s)" << " of size "
                << sizeof(T) << std::endl;
        pointer ret = (pointer) (::operator new(num * sizeof(T)));
        std::cout << " allocated at: " << (void*) ret << std::endl;
        return ret;
    }

    // deallocate storage p of deleted elements
    void deallocate(pointer p, size_type num) {
        CustomAllocatorState::m_totalMemAllocated -= num * sizeof(T);
        // print message and deallocate memory with global delete
        std::cout << "deallocate " << num << " element(s)" << " of size "
                << sizeof(T) << " at: " << (void*) p << std::endl;
        ::operator delete((void*) p);
    }

    // initialize elements of allocated storage p with value value
    // no need to call rebind with this variadic template anymore in C++11
    template<typename _U, typename ... _Args>
    void construct(_U* p, _Args&&... args) {
        ::new ((void *) p) _U(std::forward<_Args>(args)...);
    }

    // destroy elements of initialized storage p
    template<typename _U>
    void destroy(_U* p) {
        p->~_U();
    }

    // return address of values
    pointer address (reference value) const {
        return &value;
    }
    const_pointer address (const_reference value) const {
        return &value;
    }


    // return maximum number of elements that can be allocated
    size_type max_size () const throw() {
        return std::numeric_limits<std::size_t>::max() / sizeof(T);
    }
};

template<typename T, typename U>
inline bool operator==(const CustomAllocator<T>&, const CustomAllocator<U>&) {
    return true;
}

template<typename T, typename U>
inline bool operator!=(const CustomAllocator<T>&, const CustomAllocator<U>&) {
    return false;
}

int main() {
    std::list<int, CustomAllocator<int>> foo;
    std::list<int> bar; // aka  std::list<int, std::allocator<int> > bar;
    foo.push_back(23);
    foo.push_back(12);
    foo.push_back(8);

    // transfer/move element in foo at end() to list bar at position begin()
    auto pos = bar.begin();
    auto other = foo;
    auto it = --(foo.end());
    bar.splice(pos, foo, it);   // here the error: no matching function for call

    std::cout << "---" << std::endl;

    // debug output
    std::cout << "Foo: ";
    for (auto x : foo)
        std::cout << x << " ";
    std::cout << std::endl;

    std::cout << "Bar: ";
    for (auto x : bar)
        std::cout << x << " ";
    std::cout << std::endl;

    std::cout << "alloc_count: " << CustomAllocatorState::m_totalMemAllocated << std::endl;
    std::cout << "---" << std::endl;
    std::cout << '\n';
    return 0;
}

Solution

  • A list allocates internal nodes by given allocator. Having two lists with different allocators and trying to move elements from one to another means:

    • allocate an internal node in List1 by Allocator1
    • move the node to List2
    • later destroy the node in List2 by Allocator2

    Allocators often do some tricks, like allocating more memory than required for additional checks in debug mode e.g. for protection from buffer underruns/overruns. Only allocator knows how much memory it allocated. So Allocator2 is not suitable for deallocation something not allocated by it.

    In this particular case Allocator is a template parameter which is part of the type and makes std::list<T, Allocator1> not compatible with std::list<T, Allocator2>.

    Someone can argue that your CustomAllocator allocates memory using global operator new so should be "compatible" with default allocator. I don't know any way to make them compatible from C++ point of view, and I don't think it would be a good idea as it opens a possibility that later another person can decide to enhance you custom allocator a little bit making it incompatible with the default one accidentally. Compiler won't help you here and consequences can be devastating.