views:

1897

answers:

4

I'm trying to write a custom STL allocator that is derived from std::allocator, but somehow all calls to allocate() go to the base class. I have narrowed it down to this code:

template <typename T> class a : public std::allocator<T> {
public:
    T* allocate(size_t n, const void* hint = 0) const {
        cout << "yo!";
        return 0;
    }
};

int main()
{
    vector<int, a<int>> v(1000, 42);
    return 0;
}

I expect "Yo!" to get printed, followed by some horrible error because I don't actually allocate anything. Instead, the program runs fine and prints nothing. What am I doing wrong?

I get the same results in gcc and VS2008.

A: 

The following code prints "yo" as expected - what you were seeing was our old friend "undefined behaviour".

#include <iostream>
#include <vector>
using namespace std;

template <typename T> class a : public std::allocator<T> {
public:
    T* allocate(size_t n, const void* hint = 0) const {
        cout << "yo!";
        return new T[10000];
    }
};

int main()
{
    vector<int, a<int> > v(1000, 42);
    return 0;
}

Edit: I just checked out the C++ Standard regarding the default allocator. There is no prohibition on inheriting from it. In fact, as far as I'm aware, there is no such prohibition in any part of the Standard.

anon
This didn't work for me. VS 2008.
Tim Matthews
Did you test this code? It cannot work as it's basically the same than the question. I just run it too in debug and release mode and it don't work.
Klaim
Works fine for me with i686-apple-darwin8-g++-4.0.1
Adam Rosenfield
Yes I did test it g+++ 3.4.5 - zab
anon
So, in light of this and marvinalone's discovery that he needs to implement rebind<U>, whether the above code works appears to depend on whether the implementation of vector<T> uses rebind internally.
j_random_hacker
+4  A: 

In this case, the problem is that I didn't override the rebind member of the allocator. This version works (in VS2008):

template <typename T> class a : public std::allocator<T> {
public:
    T* allocate(size_t n, const void* hint = 0) const {
        cout << "yo!";
        return 0;
    }

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

int main() {
    vector<int, a<int>> v(1000, 42);
    return 0;
}

I found this by debugging through the STL headers.

Whether this works or not will be completely dependent on the STL implementation though, so I think that ultimately, Klaim is right in that this shouldn't be done this way.

Marvin
Actually virtual function dispatch is not used for allocator calls since the type is known at compile time, so I believe that this should now always work. The only reason not to bother inheriting from std::allocator is because it doesn't actually save you much typing!
j_random_hacker
... but see litb's answer for another requirement (template copy ctor) that you need to implement.
j_random_hacker
+3  A: 

You will need to provide a rebind member template and the other stuff that is listed in the allocator requirements in the C++ Standard. For example, you need a template copy constructor which accepts not only allocator<T> but also allocator<U>. For example, one code might do, which a std::list for example is likely to do

template<typename Allocator>
void alloc1chunk(Allocator const& alloc) {
    typename Allocator::template rebind<
        wrapper<typename Allocator::value_type>
      >::other ot(alloc);
    // ...
}

The code will fail if there either exist no correct rebind template, or there exist no corresponding copy constructor. You will get nowhere useful with guessing what the requirements are. Sooner or later you will have to do with code that relies on one part of those allocator requirements, and the code will fail because your allocator violates them. I recommend you take a look at them in some working draft your your copy of the Standard in 20.1.5.

Johannes Schaub - litb
Good point about reading the actual interface requirements -- otherwise you'll never know *for sure* that you're handling everything that an allocator is supposed to do.
j_random_hacker
+1  A: 

I have two templates for creating customized allocators; the first works automagically if it is used on a custom type:

template<>
class std::allocator<MY_TYPE>
{
public:
    typedef size_t      size_type;
    typedef ptrdiff_t   difference_type;
    typedef MY_TYPE*    pointer;
    typedef const MY_TYPE*  const_pointer;
    typedef MY_TYPE&    reference;
    typedef const MY_TYPE&  const_reference;
    typedef MY_TYPE     value_type;

    template <class U>
    struct rebind
    {
        typedef std::allocator<U> other;
    };

    pointer allocate(size_type n, std::allocator<void>::const_pointer hint = 0)
    {
        return reinterpret_cast<pointer>(ALLOC_FUNC(n * sizeof(T)));
    }
    void construct(pointer p, const_reference val)
    {
        ::new(p) T(val);
    }
    void destroy(pointer p)
    {
        p->~T();
    }
    void deallocate(pointer p, size_type n)
    {
        FREE_FUNC(p);
    }
    size_type max_size() const throw()
    {
        // return ~size_type(0); -- Error, fixed according to Constantin's comment
        return std::numeric_limits<size_t>::max()/sizeof(MY_TYPE);
    }
};

The second is used when we want to have our own allocator for a predefined type with a standard allocator, for instance char, wchar_t, std::string, etc.:

    namespace MY_NAMESPACE
    {

    template <class T> class allocator;

    // specialize for void:
    template <>
    class allocator<void>
    {
    public:
        typedef void*       pointer;
        typedef const void* const_pointer;
        // reference to void members are impossible.
        typedef void        value_type;

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

    template <class T>
    class allocator
    {
    public:
        typedef size_t      size_type;
        typedef ptrdiff_t   difference_type;
        typedef T*      pointer;
        typedef const T*    const_pointer;
        typedef T&      reference;
        typedef const T&    const_reference;
        typedef T       value_type;

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

        allocator() throw()
        {
        }
        template <class U>
        allocator(const allocator<U>& u) throw()
        {
        }
        ~allocator() throw()
        {
        }

        pointer address(reference r) const
        {
            return &r;
        }
        const_pointer address(const_reference r) const
        {
            return &r;
        }
        size_type max_size() const throw()
        {
            // return ~size_type(0); -- Error, fixed according to Constantin's comment
            return std::numeric_limits<size_t>::max()/sizeof(T);
        }
        pointer allocate(size_type n, allocator<void>::const_pointer hint = 0)
        {
            return reinterpret_cast<pointer>(ALLOC_FUNC(n * sizeof(T)));
        }
        void deallocate(pointer p, size_type n)
        {
            FREE_FUNC(p);
        }

        void construct(pointer p, const_reference val)
        {
            ::new(p) T(val);
        }
        void destroy(pointer p)
        {
            p->~T();
        }
    };

template <class T1, class T2>
inline
bool operator==(const allocator<T1>& a1, const allocator<T2>& a2) throw()
{
    return true;
}

template <class T1, class T2>
inline
bool operator!=(const allocator<T1>& a1, const allocator<T2>& a2) throw()
{
    return false;
}

}

The first template above, for your own defined type, does not require any further handling but is used automatically by the standard container classes. The second template requires further work when used on a standard type. For std::string, for example, one have to use the following construct when declaring variables of that type (it is simplest with a typedef):

std::basic_string<char>, std::char_traits<char>, MY_NAMESPACE::allocator<char> >
Stefan Rådström
It looks like `max_size()` should return `std::numeric_limits<size_t>::max() / sizeof(MY_TYPE)`.
Constantin
@Constantin: Very good observation, thanks a lot! I shall update the code accordingly. :-)
Stefan Rådström