9

I'm playing around with std::function and custom allocators but its not behaving as I expected when I don't provide the function with an initial functor.

When I provide a custom allocator to the constructor but no initial functor, the allocator is never used or so it seems.

This is my code.

//Simple functor class that is big to force allocations
struct Functor128
{
    Functor128()
    {}

    char someBytes[128];

    void operator()(int something)
    {
        cout << "Functor128 Called with value " << something << endl;
    }
};

int main(int argc, char* argv[])
{
Allocator<char, 1> myAllocator1;
Allocator<char, 2> myAllocator2;
Allocator<char, 3> myAllocator3;
Functor128 myFunctor;

cout << "setting up function1" << endl;
function<void(int)> myFunction1(allocator_arg, myAllocator1, myFunctor);
myFunction1(7);

cout << "setting up function2" << endl;
function<void(int)> myFunction2(allocator_arg, myAllocator2);
myFunction2 = myFunctor;
myFunction2(9);

cout << "setting up function3" << endl;
function<void(int)> myFunction3(allocator_arg, myAllocator3);
myFunction3 = myFunction1;
myFunction3(19);
}

Output:

setting up function1
Allocator 1 allocating 136 bytes.
Functor128 Called with value 7

setting up function2
Functor128 Called with value 9

setting up function3
Allocator 1 allocating 136 bytes.
Functor128 Called with value 19

So case1: myFunction1 allocates using allocator1 as expected.

case2: myFunction2 is given allocator2 in constructor but when assigned a functor it appears to reset to using the default std::allocator to make the allocation.(hence no print out about allocation).

case3: myFunction3 is given allocator3 in constructor but when assigned to from myFunction1 the allocation takes place using function1's allocator to make the allocation.

Is this correct behaviour? In particular, in case 2 why revert to using default std::allocator? If so what is the point of the empty constructor that takes an allocator as the allocator never gets used.

I am using VS2013 for this code.

My Allocator class is just a minimal implementation that uses new and logs out when it allocates

template<typename T, int id = 1>
class Allocator {
public:
    //    typedefs
    typedef T value_type;
    typedef value_type* pointer;
    typedef const value_type* const_pointer;
    typedef value_type& reference;
    typedef const value_type& const_reference;
    typedef std::size_t size_type;
    typedef std::ptrdiff_t difference_type;

public:
    //    convert an allocator<T> to allocator<U>
    template<typename U>
    struct rebind {
        typedef Allocator<U> other;
    };

public:
    inline  Allocator() {}
    inline ~Allocator() {}
    inline  Allocator(Allocator const&) {}
    template<typename U>
    inline  Allocator(Allocator<U> const&) {}

    //    address
    inline pointer address(reference r) { return &r; }
    inline const_pointer address(const_reference r) { return &r; }

    //    memory allocation
    inline pointer allocate(size_type cnt,
        typename std::allocator<void>::const_pointer = 0) 
    {
        size_t numBytes = cnt * sizeof (T);
        std::cout << "Allocator " << id <<  " allocating " << numBytes << " bytes." << std::endl;
        return reinterpret_cast<pointer>(::operator new(numBytes));
    }
    inline void deallocate(pointer p, size_type) {
        ::operator delete(p);
    }

    //    size
    inline size_type max_size() const {
        return std::numeric_limits<size_type>::max() / sizeof(T);
    }

    //    construction/destruction
    inline void construct(pointer p, const T& t) { new(p)T(t); }
    inline void destroy(pointer p) { p->~T(); }

    inline bool operator==(Allocator const&) { return true; }
    inline bool operator!=(Allocator const& a) { return !operator==(a); }
};    //    end of class Allocator
David Woo
  • 749
  • 4
  • 13
  • There's a proposal to remove allocator support from `std::function`: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0302r0.html with a helpful list of known issues – Andriy Tylychko Jul 31 '17 at 14:40

1 Answers1

6

std::function's allocator support is...weird.

The current spec for operator=(F&& f) is that it does std::function(std::forward<F>(f)).swap(*this);. As you can see, this means that memory for f is allocated using whatever std::function uses by default, rather than the allocator used to construct *this. So the behavior you observe is correct, though surprising.

Moreover, since the (allocator_arg_t, Allocator) and (allocator_arg_t, Allocator, nullptr_t) constructors are noexcept, they can't really store the allocator even if they wanted to (type-erasing an allocator may require a dynamic allocation). As is, they are basically no-ops that exist to support the uses-allocator construction protocol.

LWG very recently rejected an issue that would change this behavior.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • They pay lip service to the uses-allocator construction protocol (and `std::scoped_allocator_adaptor`), but they don't actually support it. Perhaps this is why GCC chooses not to implement the lip service. I'm proposing an alternative solution: http://bit.ly/allocfun (PDF). – Potatoswatter Sep 17 '15 at 10:17
  • Is it prescribed that a callable object saved in `std::function` should be deleted by the allocator which `std::function` use? Or just use `delete p`? – linux40 Dec 21 '15 at 13:46
  • @linux40 Allocators can't do `construct` or `destroy` on arbitrary types after type erasure (but see [LWG2502](http://wg21.link/LWG2502), which talks about `construct` but applies equally to `destroy`). The deallocation obviously must be done using the allocator; the destructor will likely be just called directly. – T.C. Dec 21 '15 at 14:04
  • But, as you say, `std::function` can't really store the allocator, is there any way to use this allocator? In my opinion, `std::function` could store a allocator if it use the virtual class implementation, so `construct` or `destroy` can alse be used. – linux40 Dec 21 '15 at 14:50
  • @linux40 Oh, you mean those two particular overloads? They are just messed up. Note that there's nothing for them to deallocate anyway, since assigning a target will use a different allocation mechanism. – T.C. Dec 21 '15 at 14:55
  • What about this? I wrote it just now. https://gist.github.com/anonymous/9f548329f5c6051b38ee – linux40 Dec 21 '15 at 16:10
  • @linux40 I'm not sure what point you are trying to make. Yes, as LWG2502 points out, you can do `construct`/`destroy` if you are type-erasing the allocator at the same time you are making the function object. The point is that you can't do it *afterwards*, when you replace the target. – T.C. Dec 21 '15 at 16:14
  • Yes, I erase the type of allocator at the same time, and when I replace the target, I also replace the allocator, that means a callable object binds a allocator. – linux40 Dec 21 '15 at 16:23