1

I recently had some interest for std::allocator, thinking it might solve an issue I had with some design decision on C++ code.

Now I've read some documentation about it, watched some videos, like Andrei Alexandrescu's one at CppCon 2015, and I now basically understand I shouldn't use them, because they're not designed to work the way I think allocators might work.

That being said, before realising this, I write some test code to see how a custom subclass of std::allocator could work.

Obviously, didn't work as expected... : )

So the question is not about how allocators should be used in C++, but I'm just curious to learn exactly why my test code (provided below) is not working.
Not because I want to use custom allocators. Just curious to see the exact reason...

typedef std::basic_string< char, std::char_traits< char >, TestAllocator< char > > TestString;

int main( void )
{
    TestString s1( "hello" );
    TestString s2( s1 );

    s1 += ", world";

    std::vector< int, TestAllocator< int > > v;

    v.push_back( 42 );

    return 0;
}

Complete code for TestAllocator is provided at the end of this question.

Here I'm simply using my custom allocator with some std::basic_string, and with std::vector.

With std::basic_string, I can see an instance of my allocator is actually created, but no method is called...
So it just looks like it's not used at all.

But with std::vector, my own allocate method is actually being called.

So why is there a difference here?

I did try with different compilers and C++ versions. Looks like the old GCC versions, with C++98, do call allocate on my TestString type, but not the new ones with C++11 and later. Clang also don't call allocate.

So just curious to see an explanation about these different behaviours.

Allocator code:

template< typename _T_ >
struct TestAllocator
{
    public:

        typedef       _T_   value_type;
        typedef       _T_ * pointer;
        typedef const _T_ * const_pointer;
        typedef       _T_ & reference;
        typedef const _T_ & const_reference;

        typedef std::size_t    size_type;
        typedef std::ptrdiff_t difference_type;
        typedef std::true_type propagate_on_container_move_assignment;
        typedef std::true_type is_always_equal;

        template< class _U_ >
        struct rebind
        {
            typedef TestAllocator< _U_ > other;
        };

        TestAllocator( void ) noexcept
        {
            std::cout << "CTOR" << std::endl;
        }

        TestAllocator( const TestAllocator & other ) noexcept
        {
            ( void )other;

            std::cout << "CCTOR" << std::endl;
        }

        template< class _U_ > 
        TestAllocator( const TestAllocator< _U_ > & other ) noexcept
        {
            ( void )other;

            std::cout << "CCTOR" << std::endl;
        }

        ~TestAllocator( void )
        {
            std::cout << "DTOR" << std::endl;
        }

        pointer address( reference x ) const noexcept
        {
            return std::addressof( x );
        }

        pointer allocate( size_type n, std::allocator< void >::const_pointer hint = 0 )
        {
            pointer p;

            ( void )hint;

            std::cout << "allocate" << std::endl;

            p = new _T_[ n ]();

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

            return p;
        }

        void deallocate( _T_ * p, std::size_t n )
        {
            ( void )n;

            std::cout << "deallocate" << std::endl;

            delete[] p;
        }

        const_pointer address( const_reference x ) const noexcept
        {
            return std::addressof( x );
        }

        size_type max_size() const noexcept
        {
            return size_type( ~0 ) / sizeof( _T_ );
        }

        void construct( pointer p, const_reference val )
        {
            ( void )p;
            ( void )val;

            std::cout << "construct" << std::endl;
        }

        void destroy( pointer p )
        {
            ( void )p;

            std::cout << "destroy" << std::endl;
        }
};

template< class _T1_, class _T2_ >
bool operator ==( const TestAllocator< _T1_ > & lhs, const TestAllocator< _T2_ > & rhs ) noexcept
{
    ( void )lhs;
    ( void )rhs;

    return true;
}

template< class _T1_, class _T2_ >
bool operator !=( const TestAllocator< _T1_ > & lhs, const TestAllocator< _T2_ > & rhs ) noexcept
{
    ( void )lhs;
    ( void )rhs;

    return false;
}
Macmade
  • 52,708
  • 13
  • 106
  • 123

1 Answers1

5

std::basic_string can be implemented using the small buffer optimization (a.k.a. SBO or SSO in the context of strings) - this means that it internally stores a small buffer that avoids allocations for small strings. This is very likely the reason your allocator is not being used.

Try changing "hello" to a longer string (more than 32 characters) and it will probably invoke allocate.

Also note that the C++11 standard forbids std::string to be implemented in a COW (copy-on-write) fashion - more information in this question: "Legality of COW std::string implementation in C++11"


The Standard forbids std::vector to make use of the small buffer optimization: more information can be found in this question: "May std::vector make use of small buffer optimization?".

Community
  • 1
  • 1
Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • Absolutely awesome and correct! I did not expect such a quick answer, but thank you very much. : ) Is SBO explicitly permitted by the C++ standard, or is it just something compilers do? If permitted, in which section? Tried to search for it in my C++11 copy, but no luck... – Macmade Mar 09 '17 at 21:48
  • 1
    @Macmade: **I'm not entirely sure about this**, but I **suppose** that SBO is permitted but not required. Therefore it's unlikely that you'll find something like *"`std::string` must be implemented with SBO"* in the Standard - I assume it's just an "obvious" optimization that can be implemented while still being compliant with the Standard. – Vittorio Romeo Mar 09 '17 at 21:53
  • Makes totally sense for performance, but could also break expected behaviour... Weird I cannot find something explicit about it in the standard. – Macmade Mar 09 '17 at 21:56
  • 1
    @Macmade - There is nothing explicit about either case in the standard. It all comes from carefully reading *all* the requirements for *all* the functions for each class, and consider pre- and postconditions for each and every one. For example, `string::swap` just exchanges a "sequence of characters", while `vector::swap` exchanges the values but [must also keep all iterators, pointers, and references valid](http://stackoverflow.com/a/8191356/597607). This rules out copying any "small buffers". – Bo Persson Mar 09 '17 at 22:53
  • Thanks for the additional note @BoPersson – Macmade Mar 10 '17 at 17:27