Questions about Hinnant's stack allocator

2019-01-08 08:21发布

问题:

I've been using Howard Hinnant's stack allocator and it works like a charm, but some details of the implementation are a little unclear to me.

  1. Why are global operators new and delete used? The allocate() and deallocate() member functions use ::operator new and ::operator delete respectively. Similarly, the member function construct() uses the global placement new. Why not allow for any user-defined global or class-specific overloads?
  2. Why is alignment set to hard-coded 16 bytes instead of std::alignment_of<T>?
  3. Why do the constructors and max_size have a throw() exception specification? Isn't this discouraged (see e.g. More Effective C++ Item 14.)? Is it really necessary to terminate and abort when an exception occurs in the allocator? Does this change with the new C++11 noexcept keyword?
  4. The construct() member function would be an ideal candidate for perfect forwarding (to the constructor that is being called). Is this the way to write C++11 conformant allocators?
  5. What other changes are necessary to make the current code C++11 conformant?

回答1:

I've been using Howard Hinnant's stack allocator and it works like a charm, but some details of the implementation are a little unclear to me.

Glad it's been working for you.

1. Why are global operators new and delete used? The allocate() and deallocate() member functions use ::operator new and ::operator delete respectively. Similarly, the member function construct() uses the global placement new. Why not allow for any user-defined global or class-specific overloads?

There's no particular reason. Feel free to modify this code in whatever way works best for you. This was meant to be more of an example, and it is by no means perfect. The only requirements are that the allocator and deallocator supply properly aligned memory, and that the construct member constructs an argument.

In C++11, the construct (and destroy) members are optional. I would encourage you to remove them from the allocator if you're operating in an environment that supplies allocator_traits. To find out, just remove them and see if things still compile.

2. Why is alignment set to hard-coded 16 bytes instead of std::alignment_of<T>?

std::alignment_of<T> would probably work fine. I was probably being paranoid that day.

3. Why do the constructors and max_size have a throw() exception specification? Isn't this discouraged (see e.g. More Effective C++ Item 14.)? Is it really necessary to terminate and abort when an exception occurs in the allocator? Does this change with the new C++11 noexcept keyword?

These members just won't ever throw. For C++11 I should update them to noexcept. In C++11 it becomes more important to decorate things with noexcept, especially special members. In C++11 one can detect whether an expression is nothrow or not. Code can branch depending on that answer. Code that is known to be nothrow is more likely to cause generic code to branch to a more efficient path. std::move_if_noexcept is the canonical example in C++11.

Don't use throw(type1, type2) ever. It has been deprecated in C++11.

Do use throw() when you really want to say: This will never throw, and if I'm wrong, terminate the program so I can debug it. throw() is also deprecated in C++11, but has a drop-in replacement: noexcept.

4. The construct() member function would be an ideal candidate for perfect forwarding (to the constructor that is being called). Is this the way to write C++11 conformant allocators?

Yes. However allocator_traits will do it for you. Let it. The std::lib has already debugged that code for you. C++11 containers will call allocator_traits<YourAllocator>::construct(your_allocator, pointer, args...). If your allocator implements these functions, allocator_traits will call your implementation, else it calls a debugged, efficient, default implementation.

5. What other changes are necessary to make the current code C++11 conformant?

To tell you the truth, this allocator isn't really C++03 or C++11 conformant. When you copy an allocator, the original and the copy are supposed to be equal to each other. In this design, that is never true. However this thing still just happens to work in many contexts.

If you want to make it strictly conforming, you need another level of indirection such that copies will point to the same buffer.

Aside from that, C++11 allocators are so much easier to build than C++98/03 allocators. Here's the minimum you must do:

template <class T>
class MyAllocator
{
public:
    typedef T value_type;

    MyAllocator() noexcept;  // only required if used
    MyAllocator(const MyAllocator&) noexcept;  // copies must be equal
    MyAllocator(MyAllocator&&) noexcept;  // not needed if copy ctor is good enough
    template <class U>
        MyAllocator(const MyAllocator<U>& u) noexcept;  // requires: *this == MyAllocator(u)

    value_type* allocate(std::size_t);
    void deallocate(value_type*, std::size_t) noexcept;
};

template <class T, class U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) noexcept;

template <class T, class U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) noexcept;

You might optionally consider making MyAllocator Swappable and put the following nested type in the allocator:

typedef std::true_type propagate_on_container_swap;

There's a few other knobs like that you can tweak on C++11 allocators. But all of the knobs have reasonable defaults.

Update

Above I note that my stack allocator is not conforming due to the fact that copies are not equal. I've decided to update this allocator to a conforming C++11 allocator. The new allocator is called short_allocator and is documented here.

The short_allocator differs from the stack allocator in that the "internal" buffer is no longer internal to the allocator, but is now a separate "arena" object that can be located on the local stack, or given thread or static storage duration. The arena isn't thread safe though so watch out for that. You could make it thread safe if you wanted to, but that has diminishing returns (eventually you'll reinvent malloc).

This is conforming because copies of allocators all point to the same external arena. Note that the unit of N is now bytes, not number of T.

One could convert this C++11 allocator to a C++98/03 allocator by adding the C++98/03 boiler-plate (the typedefs, the construct member, the destroy member, etc.). A tedious, but straightforward task.

The answers to this question for the new short_allocator remain unchanged.