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.
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.