c++containersallocator

When to call the select_on_container_copy_construction for a container?


I'm trying to design an allocator aware container from scratch. The std::allocator_traits class has a member method called select_on_container_copy_construction(..). I saw a question covering the reason for its existence. But, I couldn't get where I should use this method. Here is a simplified custom container with some notes on it.

/*** Libraries ***/
#include <memory>       // std::allocator, std::allocator_traits

/*** Container Class ***/
template<class T, class Allocator = std::allocator<T>>
class CustomContainer{
public:
    using size_type         = typename std::allocator_traits<Allocator>::size_type;
    using pointer           = typename std::allocator_traits<Allocator>::pointer;

    /*** Constructors and Destructor ***/
    // Default constructor
    CustomContainer() = default;

    // Copy constructor
    CustomContainer(const CustomContainer& other)
        : allocator(std::allocator_traits<Allocator>::select_on_container_copy_construction(other.allocator))
    {
        // Copy construction algorithm here...
    }

    // Move constructor
    CustomContainer(CustomContainer&& other)
        : allocator(/* What to do here? */)
    {
        // Move construction algorithm here...
    }

    /*** Operators ***/
    CustomContainer& operator=(const CustomContainer& other)
    {
        allocator = other.allocator;  // Is that true?

        // Copy assignment algorithm here...
    }

    /*** Member Methods ***/
    // Some member methods here..

private:
    /*** Members ***/
    pointer     data    = nullptr;
    std::size_t sz      = 0;
    Allocator   allocator;
};

As you can see that I called the select_on_container_copy_construction(..) method inside the initialization list of the copy constructor.


Solution

  • As specified in the Allocator requirements, the select_on_container_copy_construction() function provides an instance of the allocator to be used by the container that is copy constructed from another instance of the same container type.

    This means that the copy constructor must always obtain the allocator of the source container through the above function. On the other hand, it must not be used by neither the allocator-extended copy constructor nor other member functions.

    The copy assignment operator, move assignment operator and swap() member function must be able to handle the depending on POCCA, POCMA and POCS, respectively.

    An example of implementation for a simplified allocator-aware container is the following.

    template <typename T, typename Alloc = std::allocator<T>>
    class Container
    {
    private:
      using alloc_t               = typename std::allocator_traits<Alloc>::rebind_alloc<Alloc>;
      using alloc_traits_t        = std::allocator_traits<alloc_t>;
    
    public:
      using value_type            = T;
      using pointer               = alloc_traits_t::pointer;
      using const_pointer         = alloc_traits_t::const_pointer;
      using reference             = T&;
      using const_reference       = const T&;
      using size_type             = alloc_traits_t::size_type;
      using difference_type       = alloc_traits_t::difference_type;
      using allocator_type        = Alloc;
    
      Container() = default;
    
      template <typename U>
      explicit Container(U&& u, const allocator_type& a = allocator_type())
       : _ptr{}, _alloc{a}
      {
        this->_ptr = alloc_traits_t::allocate(this->_alloc, 1);
        alloc_traits_t::construct(this->_alloc, std::to_address(this->_ptr), std::forward<U>(u));
      }
    
      Container(const Container& x)
       : _ptr{}, _alloc{alloc_traits_t::select_on_container_copy_consyruction(x._alloc)}
      {
        this->_ptr = alloc_traits_t::allocate(this->_alloc, 1);
        alloc_traits_t::construct(this->_alloc, std::to_address(this->_ptr), *x._ptr);
      }
    
      Container(const Container& x, const allocator_type& a)
       : _ptr{}, _alloc{a}
      {
        this->_ptr = alloc_traits_t::allocate(this->_alloc, 1);
        alloc_traits_t::construct(this->_alloc, std::to_address(this->_ptr), *x._ptr);
      }
    
      Container(Container&& x) noexcept
       : _ptr{}, _alloc{std::move(x._alloc)}
      { std::swap(this->_ptr, x._ptr); }
    
      Container(Container&& x, const allocator_type& a)
       : _ptr{}, _alloc{a}
      {
        if(this->_alloc != x._alloc){
          this->_ptr = alloc_traits_t::allocate(this->_alloc, 1);
          alloc_traits_t::construct(this->_alloc, std::to_address(this->_ptr), std::move(*x._ptr));
        }
        else
          std::swap(this->_ptr, x._ptr);
      }
    
      ~Container() noexcept
      {
        if(this->_ptr != nullptr){
          alloc_traits_t::destroy(this->_alloc, std::to_address(this->_ptr));
          alloc_traits_t::deallocate(this->_alloc, this->_ptr, 1);
        }
      }
    
      Container& operator=(const Container& x)
      {
        if constexpr(alloc_traits_t::propagate_on_container_copy_assignment && !alloc_traits_t::is_always_equal){
          if(this->_alloc != x._alloc){
            if(this->_ptr != nullptr){
              alloc_traits_t::destroy(this->_alloc, std::to_address(this->_ptr));
              alloc_traits_t::deallocate(this->_alloc, this->_ptr, 1);
            }
            this->_ptr = alloc_traits_t::allocate(x._alloc, 1);
            alloc_traits_t::construct(x._alloc, std::to_address(this->_ptr), *x._ptr);
          }
          this->_alloc = x._alloc;
        }
        else
          *this->_ptr = *x._ptr;
        return *this;
      }
    
      Container& operator=(Container&& x)
        noexcept(alloc_traits_t::propagate_on_container_move_assignment
                || alloc_traits_t::is_always_equal)
      {
        if constexpr(alloc_traits_t::propagate_on_container_move_assignment && !alloc_traits_t::is_always_equal){
          std::swap(this->_alloc, x._alloc);
          std::swap(this->_ptr, x._ptr);
        }
        else{
          if(this->_alloc != x._alloc)
            *this->_ptr = std::move(*x._ptr);
          else
            std::swap(this->_ptr, x._ptr);
        }
        return *this;
      }
    
      void swap(Container& x) noexcept
      {
        if constexpr(alloc_traits_t::propagate_on_container_swap && !alloc_traits_t::is_always_equal)
          std::swap(this->_alloc, x._alloc);
        std::swap(this->_ptr, x._ptr);
      }
    
      allocator_type get_allocator() const noexcept
      { return this->_alloc; }
    
    private:
      pointer  _ptr;
      alloc_t  _alloc;
    };
    

    The container stores an instance of the rebound allocator, alloc_t. In this case, it is the same type as the original, but a generic allocator-aware container may require to use a different allocator to perform allocate and deallocate operations. For this reason, it is advisable to always store and use the rebound version of the allocator.

    Another important consideration is the full support for fancy pointers: since the allocator type may use classes that behave like pointers, even if they are not, it is necessary to correctly handle them. Indeed, fancy pointers may contain information that would be lost if they were converted to native pointers, potentially invalidating access to objects and deallocate operations.