Garmaine Staff asked 2 years ago

Class templates in the ::std namespace can generally be specialized by programs for user-defined types. I did not find any exception to this rule for std::allocator.

So, am I allowed to specialize std::allocator for my own types? And if I am allowed to, do I need to provide all members of std::allocator's primary template, given that many of them can be provided by std::allocator_traits (and are consequently deprecated in C++17)?

Consider this program

#include<vector>
#include<utility>
#include<type_traits>
#include<iostream>
#include<limits>
#include<stdexcept>

struct A { };

namespace std {
    template<>
    struct allocator<A> {
        using value_type = A;
        using size_type = std::size_t;
        using difference_type = std::ptrdiff_t;
        using propagate_on_container_move_assignment = std::true_type;

        allocator() = default;

        template<class U>
        allocator(const allocator<U>&) noexcept {}

        value_type* allocate(std::size_t n) {
            if(std::numeric_limits<std::size_t>::max()/sizeof(value_type) < n)
                throw std::bad_array_new_length{};
            std::cout << "Allocating for " << n << "\n";
            return static_cast<value_type*>(::operator new(n*sizeof(value_type)));
        }

        void deallocate(value_type* p, std::size_t) {
            ::operator delete(p);
        }

        template<class U, class... Args>
        void construct(U* p, Args&&... args) {
            std::cout << "Constructing one\n";
            ::new((void *)p) U(std::forward<Args>(args)...);
        };

        template<class U>
        void destroy( U* p ) {
            p->~U();
        }

        size_type max_size() const noexcept {
            return std::numeric_limits<size_type>::max()/sizeof(value_type);
        }
    };
}

int main() {
    std::vector<A> v(2);
    for(int i=0; i<6; i++) {
        v.emplace_back();
    }
    std::cout << v.size();
}

The output of this program with libc++ (Clang with -std=c++17 -Wall -Wextra -pedantic-errors -O2 -stdlib=libc++) is:

Allocating for 2
Constructing one
Constructing one
Allocating for 4
Constructing one
Constructing one
Allocating for 8
Constructing one
Constructing one
Constructing one
Constructing one
8

and the output with libstdc++ (Clang with -std=c++17 -Wall -Wextra -pedantic-errors -O2 -stdlib=libstdc++) is:

Allocating for 2
Allocating for 4
Constructing one
Constructing one
Allocating for 8
Constructing one
Constructing one
Constructing one
Constructing one
8

As you can see libstdc++ does not always honor the overload of construct that I have provided and if I remove the construct, destroy or max_size members, then the program doesn't even compile with libstdc++ complaining about these missing members, although they are supplied by std::allocator_traits.

Does the program have undefined behavior and are therefore both standard libraries correct, or is the program's behavior well-defined and the standard library required to use my specialization?

Note that there are some members from std::allocator's primary template that I still left out in my specialization. Do I need to add them as well?