15

This question is about construction of instances of custom allocator during insertion into a std::map.

Here is a custom allocator for std::map<int,int> along with a small program that uses it:

#include <stddef.h>
#include <stdio.h>
#include <map>
#include <typeinfo>

class MyPool {
public:
  void * GetNext() {
    return malloc(24);
  }
  void Free(void *ptr) {
    free(ptr);
  }
};

template<typename T>
class MyPoolAlloc {
public:
  static MyPool *pMyPool;

  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<typename X>
  struct rebind
  { typedef MyPoolAlloc<X> other; };

  MyPoolAlloc() throw() {
    printf("-------Alloc--CONSTRUCTOR--------%08x %32s\n", this, typeid(T).name());
  }

  MyPoolAlloc(const MyPoolAlloc&) throw()  {
    printf(" Copy Constructor ---------------%08x %32s\n", this, typeid(T).name());
  }

  template<typename X>
  MyPoolAlloc(const MyPoolAlloc<X>&) throw() {
    printf(" Construct T Alloc from X Alloc--%08x %32s %32s\n", this, typeid(T).name(), typeid(X).name());
  }

  ~MyPoolAlloc() throw() {
    printf(" Destructor ---------------------%08x %32s\n", this, typeid(T).name());
  };

  pointer address(reference __x) const { return &__x; }

  const_pointer address(const_reference __x) const { return &__x; }

  pointer allocate(size_type __n, const void * hint = 0) {
    if (__n != 1)
      perror("MyPoolAlloc::allocate: __n is not 1.\n");
    if (NULL == pMyPool) {
      pMyPool = new MyPool();
      printf("======>Creating a new pool object.\n");
    }
    return reinterpret_cast<T*>(pMyPool->GetNext());
  }

  //__p is not permitted to be a null pointer
  void deallocate(pointer __p, size_type __n) {
    pMyPool->Free(reinterpret_cast<void *>(__p));
  }

  size_type max_size() const throw() {
    return size_t(-1) / sizeof(T);
  }

  void construct(pointer __p, const T& __val) {
    printf("+++++++ %08x %s.\n", __p, typeid(T).name());
    ::new(__p) T(__val);
  }

  void destroy(pointer __p) {
    printf("-+-+-+- %08x.\n", __p);
    __p->~T();
  }
};

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

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

template<typename T>
MyPool* MyPoolAlloc<T>::pMyPool = NULL;

int main(int argc, char *argv[]) {

  std::map<int, int, std::less<int>, MyPoolAlloc<std::pair<const int,int> > > m;
  //random insertions in the map
  m.insert(std::pair<int,int>(1,2));
  m[5] = 7;
  m[8] = 11;
  printf("======>End of map insertions.\n");
  return 0;
}

Here is the output of this program:

-------Alloc--CONSTRUCTOR--------bffcdaa6                     St4pairIKiiE
 Construct T Alloc from X Alloc--bffcda77  St13_Rb_tree_nodeISt4pairIKiiEE                     St4pairIKiiE
 Copy Constructor ---------------bffcdad8  St13_Rb_tree_nodeISt4pairIKiiEE
 Destructor ---------------------bffcda77  St13_Rb_tree_nodeISt4pairIKiiEE
 Destructor ---------------------bffcdaa6                     St4pairIKiiE
======>Creating a new pool object.
 Construct T Alloc from X Alloc--bffcd9df                     St4pairIKiiE  St13_Rb_tree_nodeISt4pairIKiiEE
+++++++ 0985d028 St4pairIKiiE.
 Destructor ---------------------bffcd9df                     St4pairIKiiE
 Construct T Alloc from X Alloc--bffcd95f                     St4pairIKiiE  St13_Rb_tree_nodeISt4pairIKiiEE
+++++++ 0985d048 St4pairIKiiE.
 Destructor ---------------------bffcd95f                     St4pairIKiiE
 Construct T Alloc from X Alloc--bffcd95f                     St4pairIKiiE  St13_Rb_tree_nodeISt4pairIKiiEE
+++++++ 0985d068 St4pairIKiiE.
 Destructor ---------------------bffcd95f                     St4pairIKiiE
======>End of map insertions.
 Construct T Alloc from X Alloc--bffcda23                     St4pairIKiiE  St13_Rb_tree_nodeISt4pairIKiiEE
-+-+-+- 0985d068.
 Destructor ---------------------bffcda23                     St4pairIKiiE
 Construct T Alloc from X Alloc--bffcda43                     St4pairIKiiE  St13_Rb_tree_nodeISt4pairIKiiEE
-+-+-+- 0985d048.
 Destructor ---------------------bffcda43                     St4pairIKiiE
 Construct T Alloc from X Alloc--bffcda43                     St4pairIKiiE  St13_Rb_tree_nodeISt4pairIKiiEE
-+-+-+- 0985d028.
 Destructor ---------------------bffcda43                     St4pairIKiiE
 Destructor ---------------------bffcdad8  St13_Rb_tree_nodeISt4pairIKiiEE

Last two columns of the output show that an allocator for std::pair<const int, int> is constructed everytime there is a insertion into the map. Why is this necessary? Is there a way to suppress this?

Thanks!

Edit: This code tested on x86 machine with g++ version 4.1.2. If you wish to run it on a 64-bit machine, you'll have to change at least the line return malloc(24). Changing to return malloc(48) should work.

Prasoon Tiwari
  • 840
  • 2
  • 12
  • 23

3 Answers3

1

It is so because the allocator is for std::pair<const int, int> but the implementation actually needs to allocate a more complex data structure, of which that is a member. Whilst I'd expect the actual allocator needed to be constructed and cached, it is not illegal for it to be re-constructed every time. This is an implementation detail you cannot escape without changing your implementation. The actual allocator type that is created is St13_Rb_tree_nodeISt4pairIKiiEE (mangled name).

Scrubbins
  • 150
  • 4
  • It is clearly not illegal to construct it every time as the above output shows. Is there an inherent reason that STL/C++ must do it this way? If not, then how do you change the implementation to suppress it? – Prasoon Tiwari Jul 07 '12 at 09:46
  • 2
    In C++03, an implementation is allowed to assume than an allocator is stateless (empty). That way, creating a copy is almost free. In your case, you could possibly have the copy constructor and assignment operator copy the internal pointer, and not create a new Pool each time. – Bo Persson Jul 07 '12 at 11:26
  • @Bo - The subject of the question is not the copy costructor! Instead it is the constructor that creates an allocator for St4pairIKiiE from an allocator for St13_Rb_tree_nodeISt4pairIKiiEE. – Prasoon Tiwari Jul 07 '12 at 11:38
  • I'm referring to the assignment and copying of the allocator object. If you let the copy of the allocator share the pointer to the same `MyPool` object, it might work. – Bo Persson Jul 07 '12 at 12:08
  • @Bo - All allocators of same type share the same pool via a static pointer. In any case, there will still be one allocator constructor call per insertion; the question is how to avoid it. – Prasoon Tiwari Jul 07 '12 at 12:18
  • 1
    @Prasoon why bother? If you get rid of the tracing statements the compiler will optimise out the construction and destruction, so it won't cost you anything. – Alan Stokes Jul 07 '12 at 13:31
  • @Alan - Good observation. I came across it while trying to understand how custom allocators work. Can't figure out if this is a bug or a feature. – Prasoon Tiwari Jul 07 '12 at 14:37
  • 1
    @Prasoon it's a feature. It's to with the dreaded `rebind` member template. As Scrubbins said you have an allocator for one type and `map` needs an allocator for another type, so it has to construct one [via rebind](http://msdn.microsoft.com/en-us/library/5fk3e8ek(v=vs.80).aspx). – Alan Stokes Jul 07 '12 at 14:42
  • @Alan - rebind I understand. But why construct a St4pairIKiiE allocator again and again at every insertion? The St13_Rb_tree_nodeISt4pairIKiiEE constructor knows that its always going to "placement construct" St4pairIKiiE. – Prasoon Tiwari Jul 07 '12 at 14:51
  • @Alan - about your comment on the construction being optimized out - while the constructor call will be optimized out the corresponding memory allocation call will probably not. The reason being that this St4pairIKiiE constructor object is used in a non-trivial way. – Prasoon Tiwari Jul 07 '12 at 15:01
  • 1
    @Prasoon: "while the constructor call will be optimized out the corresponding memory allocation call will probably not." What memory allocation? The container isn't heap-allocating your allocators; they're on the stack. And your allocator isn't allocating memory on the heap in the constructor. So... what allocations are you talking about? – Nicol Bolas Jul 08 '12 at 06:59
  • @Nicol: From the addresses of the constructor objects, it does appear that they are on the stack. Is there any performance penalty from these constructions. I have to benchmark a billion (localized) inserts and deletes. – Prasoon Tiwari Jul 08 '12 at 08:02
1

In MyPool.h (singleton):

class MyPool
{
...
public:
  static MyPool & GetInstance( void );
private:
  MyPool(void);
}

In MyPool.cpp:

MyPool & MyPool::GetInstance( void )
{
  static MyPool retval;
  return retval;
}

In fooStdAllocator.h:

#pragma once

#include "MyPool.h"

#pragma push_macro( "new" )
#undef new
#include <new>

template <class T1> class fooStdAllocator;

// Description:
// Specialize for void
template <> class fooStdAllocator<void>
{
public:
  typedef void * pointer;
  typedef const void* const_pointer;
  typedef void value_type;
  template <class U1> struct rebind { typedef fooStdAllocator<U1> other; };
};

template <class T1> class fooStdAllocator
{
public:
  // Description:
  // Typedefs
  typedef T1 value_type;
  typedef size_t size_type;
  typedef ptrdiff_t difference_type;
  typedef T1* pointer;
  typedef const T1* const_pointer;
  typedef T1& reference;
  typedef const T1& const_reference;

  // Description:
  // The rebind member allows a container to construct an allocator for some arbitrary type out of
  // the allocator type provided as a template parameter.
  template <class U1> struct rebind { typedef fooStdAllocator<U1> other; };

  // Description:
  // Constructors
  fooStdAllocator( void ) : pool(MyPool::GetInstance()) {};
  fooStdAllocator( const fooStdAllocator& other ) : pool(MyPool::GetInstance()) {};
  template <class U1> fooStdAllocator(const fooStdAllocator<U1>&) : pool(MyPool::GetInstance()) {};

  // Description:
  // Destructor
  ~fooStdAllocator( void ) {};

  // Description:
  // Returns the address of r as a pointer type. This function and the following function are used
  // to convert references to pointers.
  pointer address(reference r) const { return &r; };
  const_pointer address(const_reference r) const { return &r; };

  // Description:
  // Allocate storage for n values of T1.
  pointer allocate( size_type n, fooStdAllocator<void>::const_pointer hint = 0 )
  {
    // I would never do it that way:
    //pointer return_value = reinterpret_cast<pointer>( pool.GetNext() );
    // I would prefer to use the got size to allocate:
    pointer return_value = reinterpret_cast<pointer>( pool.GetNext(n) );

    if ( return_value == 0 )
      throw std::bad_alloc();
    return return_value;
  };

  // Description:
  // Deallocate storage obtained by a call to allocate.
  void deallocate(pointer p, size_type n)
  {
    pool.Free(p);
  };

  // Description:
  // Return the largest possible storage available through a call to allocate.
  size_type max_size() const
  {
    size_type return_value = 0xFFFFFFFF;
    return_value /= sizeof(T1);
    return return_value;
  };

  // Description:
  // Construct an object of type T1 at the location of ptr
  void construct(pointer ptr)
  {
    ::new (reinterpret_cast<void*>(ptr)) T1;
  };

  // Description:
  // Construct an object of type T1 at the location of ptr, using the value of U1 in the call to the
  // constructor for T1.
  template <class U1> void construct(pointer ptr, const U1& val)
  {
    ::new (reinterpret_cast<void*>(ptr)) T1(val);
  };

  // Description:
  // Construct an object of type T1 at the location of ptr, using the value of T1 in the call to the
  // constructor for T1.
  void construct(pointer ptr, const T1& val)
  {
    ::new (reinterpret_cast<void*>(ptr)) T1(val);
  };

  // Description:
  // Call the destructor on the value pointed to by p
  void destroy(pointer p)
  {
    p->T1::~T1();
  };
private:
  MyPool &pool;
};

// Return true if allocators b and a can be safely interchanged. "Safely interchanged" means that b could be
// used to deallocate storage obtained through a and vice versa.
template <class T1, class T2> bool operator == ( const fooStdAllocator<T1>& a, const fooStdAllocator<T2>& b)
{
  return true;
};
// Return false if allocators b and a can be safely interchanged. "Safely interchanged" means that b could be
// used to deallocate storage obtained through a and vice versa.
template <class T1, class T2> bool operator != ( const fooStdAllocator<T1>& a, const fooStdAllocator<T2>& b)
{
  return false;
};
#pragma pop_macro( "new" )

You could use it as follows:

std::map<keyT,valueT,std::less<keyT>,fooStdAllocator> your_map;
Naszta
  • 7,560
  • 2
  • 33
  • 49
  • I notice that 2 of the 3 construct functions do not need reinterpret_cast<>. Can you explain why this code should work? (I will not have access to a machine until Monday.) – Prasoon Tiwari Jul 07 '12 at 16:11
  • @PrasoonTiwari: [Really good article on this topic.](http://www.codeguru.com/cpp/cpp/cpp_mfc/stl/article.php/c4079) – Naszta Jul 07 '12 at 16:16
  • @PrasoonTiwari: `reinterpret_cast`s: might be true (may be for `new` operators), but I copied a working and tested code for you. :) – Naszta Jul 07 '12 at 16:18
  • the allocator will work because its interface is a superset of the required alloator interface. But are you claiming that it will not construct a new allocator object for each insertion into the map? – Prasoon Tiwari Jul 07 '12 at 16:34
  • 1
    @PrasoonTiwari: if you want single instance, you should make singleton. I modify the code for you. – Naszta Jul 07 '12 at 16:36
  • its not a single instance of "MyPool" I'm after. I want to know why the allocator for St13_Rb_tree_nodeISt4pairIKiiEE is constructing an allocator for St4pairIKiiE for each insertion into the map. – Prasoon Tiwari Jul 07 '12 at 16:59
  • @PrasoonTiwari: because your STL implementation create `allocator` instance in `map::insert` method. – Naszta Jul 07 '12 at 17:04
  • 1
    Shouldn't the allocate method use `pool.GetNext(n * sizeof T1)` instead of `pool.GetNext(n)`? `map` seems to be calling `allocate(1)` for each node. – xan Dec 18 '13 at 21:57
  • @xan: as I understand, `GetNext()` does know the `sizeof(T1)`. If it doesn't, you're right. BTW: `GetNext(n)` would be the best... – Naszta Dec 20 '13 at 16:34
  • I don't see that it's possible for `MyPool` to know about `sizeof T1` in the presence of the rebinding constructor. I notice that STL map does allocate one block of a different size. – xan Jan 02 '14 at 19:03
0

I had a recent project that made me look into custom memory allocators for the C++ containers. That's when I came across this article. It was good enough to copy a set of code that required minimal changes to get working.

I am going to subtly change the code example of Naszta which was drafted by Prasoon Tiwari. The first change was changing MyPool to be a templated class. Sorry that my example will include a name change. Here is the template:

#pragma once
#include <cstdint>

//class MyPool
template  <class T1>
class FooPool
{
public:
    typedef T1 value_type;
    typedef T1* pointer;
    typedef const T1* const_pointer;
    typedef T1& reference;
    typedef const T1& const_reference;

    static FooPool& GetInstance()
    {
        static FooPool myPool;

        return myPool;
    }

    pointer GetNext( size_t szItemCount )
    {
        pointer pObjects = nullptr;

        if ( szItemCount > 0 )
        {
            size_t blockSize = szItemCount * sizeof( T1 );
            uint8_t* pBytes = new uint8_t[blockSize];
            memset( pBytes, 0, blockSize );

            pObjects = reinterpret_cast<pointer>(pBytes);
        }

        return pObjects;
    }

    bool Free( pointer pObjects )
    {
        uint8_t* pBytes = reinterpret_cast<uint8_t*>(pObjects);
        delete[] pBytes;

        return true;
    }

private:
    // hide constructor to enforce singleton usage
    FooPool() = default;
    
    // this constructor will show the type of objects in this pool
    //FooPool()
    //{
    //    OutputDebugStringA( "FooPool object type: ");
    //    OutputDebugStringA( typeid(T1).name() );
    //    OutputDebugStringA( "  aka  " );
    //    OutputDebugStringA( typeid(T1).raw_name() );
    //    OutputDebugStringA( "\n" );
    //}
};

The significant change here is that FooPool knows the type of objects it is creating. This is perceived as giving it more flexibility in how it may do the memory allocation. Your compiler will show you a few other tweaks that will have to be made in the FooStdAllocator constructors.

This is now declared in FooStdAllocator as:

template <class T1>
class FooStdAllocator
{
private:
    FooPool<T1>& m_FooPool;
...
}

The last change has to do with FooStdAllocator's call to do the allocation. This was changed to:

pointer allocate( size_type n, FooStdAllocator<void>::const_pointer hint = nullptr )
{
    //pointer return_value = reinterpret_cast<pointer>(pool.GetNext( n * sizeof( T1 ) ));
    // FooPool is now templated so it now knows the size of T1
    pointer return_value = m_FooPool.GetNext( n );

    if ( return_value == nullptr )
    {
        throw std::bad_alloc();
    }

    return return_value;
}

This includes the correction by xan regarding the size of the allocation.

Scott Howard
  • 236
  • 5
  • 10