tl;dr; Currently a deleter is a facade over an allocator which currently doesn't hide how big the allocator is.
Allocators provides a customization point for a generic data structure which may allocate internal structures.
An
allocator controls allocation, construction, destruction and
deallocation. In the standard we have some opinionated allocators
std::allocator, std::scoped_allocator_adaptor and std::pmr::polymorphic_allocator.
With
current allocators, if any one of those customization points require
state then the entire allocator is stateful. This is fine if you are
using the allocator as an allocator, you are going to be using those
methods that require state so the state is not an overhead. But there
are situations, like deleter, where you are only using a subset of those
methods.
For a concrete example,
allocate_unique needs the allocator to do the allocate and construct.
But the returned unique_ptr only requires a specialized deleter based on
the provided allocator. This deleter is a facade of the allocator,
simplifying the interface and only using destroy and deallocate methods
of the allocator. If those methods are stateless in nature then the
unique_ptr should not have to carry unnecessarily state that the deleter
doesn't use.
For allocators which are partially stateful there is currently no
facility to make an allocate_unique implementation
which can sense if the deleter methods (destroy and deallocate) are
stateless. Without that sense the deleter has to be as stateful as the
allocator it was derived from.
In my
experimental codebase I have a policy_based_allocator which uses a
policy to builds the right allocator and I have facilities to sense
which parts of the allocator are stateful. Consider the following code:
#include <memory>
#include <experimental/type_traits>
#include <type_traits>
#include <vector>
template<class> struct dependent_false : std::false_type {};
// Common policy fragments (all stateless)
struct allocate_as_default
{
template<class T>
[[nodiscard]] static T * allocate(std::size_t n)
{
auto constexpr alignment = static_cast<std::align_val_t>(alignof(T));
auto const bytes = sizeof(T) * n;
return static_cast<T *>(::operator new(bytes, alignment));
}
};
struct construct_as_default
{
template<class T, class... Args>
static void construct(T * p, Args && ... args)
{
::new((void *) p) T(std::forward<Args>(args)...);
}
};
struct destroy_as_default
{
template<typename T>
static void destroy(T * p)
{
p->~T();
}
};
struct deallocate_as_default
{
template<typename T>
static void deallocate(T * p, std::size_t n) noexcept
{
::operator delete(p, alignof(T));
}
};
struct deallocate_as_noop
{
template<typename T>
static void deallocate(T * p, std::size_t n) noexcept
{
}
};
template<class Policy>
struct allocator_policy_traits
{
private:
template<class U> using detect_stateful_allocate = decltype(U::template allocate<char>(
std::declval<typename U::resource *>(), std::declval<std::size_t>()));
template<class U> using detect_stateless_allocate = decltype(U::template allocate<char>(
std::declval<std::size_t>()));
template<class U> using detect_stateful_construct = decltype(U::template construct<char>(
std::declval<typename U::resource *>(), std::declval<char *>()));
template<class U> using detect_stateless_construct = decltype(U::template construct<char>(
std::declval<char *>()));
template<class U> using detect_stateful_destroy = decltype(U::template destroy<char>(
std::declval<typename U::resource *>(), std::declval<char *>()));
template<class U> using detect_stateless_destroy = decltype(U::template destroy<char>(
std::declval<char *>()));
template<class U> using detect_stateful_deallocate = decltype(U::template deallocate<char>(
std::declval<typename U::resource *>(), nullptr, 1));
template<class U> using detect_stateless_deallocate = decltype(U::template deallocate<char>(
nullptr, 1));
template<class U> using detect_allocate_method = typename U::allocate_method;
template<class U> using detect_construct_method = typename U::construct_method;
template<class U> using detect_destroy_method = typename U::destroy_method;
template<class U> using detect_deallocate_method = typename U::deallocate_method;
public:
using has_explicit_stateful_allocate = std::experimental::is_detected<detect_stateful_allocate, Policy>;
using has_explicit_stateless_allocate = std::experimental::is_detected<detect_stateless_allocate, Policy>;
using has_explicit_stateful_construct = std::experimental::is_detected<detect_stateful_construct, Policy>;
using has_explicit_stateless_construct = std::experimental::is_detected<detect_stateless_construct, Policy>;
using has_explicit_stateful_destroy = std::experimental::is_detected<detect_stateful_destroy, Policy>;
using has_explicit_stateless_destroy = std::experimental::is_detected<detect_stateless_destroy, Policy>;
using has_explicit_stateful_deallocate = std::experimental::is_detected<detect_stateful_deallocate, Policy>;
using has_explicit_stateless_deallocate = std::experimental::is_detected<detect_stateless_deallocate, Policy>;
using requires_stateful_allocator = std::disjunction<
has_explicit_stateful_allocate,
has_explicit_stateful_construct,
has_explicit_stateful_destroy,
has_explicit_stateful_deallocate
>;
using requires_stateful_deleter = std::disjunction<
has_explicit_stateful_destroy,
has_explicit_stateful_deallocate
>;
using allocate_method = std::conditional_t<
has_explicit_stateless_allocate::value,
Policy,
std::experimental::detected_or_t<allocate_as_default, detect_allocate_method, Policy>
>;
using construct_method = std::conditional_t<
has_explicit_stateless_construct::value,
Policy,
std::experimental::detected_or_t<construct_as_default, detect_construct_method, Policy>
>;
using destroy_method = std::conditional_t<
has_explicit_stateless_destroy::value,
Policy,
std::experimental::detected_or_t<destroy_as_default, detect_destroy_method, Policy>
>;
using deallocate_method = std::conditional_t<
has_explicit_stateless_deallocate::value,
Policy,
std::experimental::detected_or_t<deallocate_as_default, detect_deallocate_method, Policy>
>;
};
template<typename Policy, typename Enable = void>
struct policy_based_allocator_base // state-less
{
using is_always_equal = std::true_type;
};
template<typename Policy>
struct policy_based_allocator_base<Policy, std::enable_if_t<allocator_policy_traits<Policy>::requires_stateful_allocator::value>> // state-full
{
using resource_t = typename Policy::resource;
using is_always_equal = std::false_type;
policy_based_allocator_base() = delete;
policy_based_allocator_base(resource_t * resource) : m_resource{resource} {} // NOLINT
policy_based_allocator_base(policy_based_allocator_base const & other) : m_resource{other.m_resource} {}
resource_t * m_resource;
};
template<typename T, typename Policy>
struct policy_based_allocator : policy_based_allocator_base<Policy>
{
using policy = Policy;
using base = policy_based_allocator_base<Policy>;
using trait = allocator_policy_traits<Policy>;
using value_type = std::remove_cv_t<T>;
template<class U> struct rebind { typedef policy_based_allocator<U, Policy> other; };
//inherit base constructors
using base::policy_based_allocator_base;
template<typename U>
policy_based_allocator(policy_based_allocator<U, Policy> const & other) : base(other) {} // NOLINT
[[nodiscard]] value_type * allocate(std::size_t n)
{
if constexpr(trait::has_explicit_stateful_allocate::value)
{
// Stateful allocate
return Policy::template allocate<value_type>(base::m_resource, n);
}
else
{
return trait::allocate_method::template allocate<value_type>(n);
}
}
template<class U, class... Args>
void construct(U * p, Args && ... args)
{
constexpr bool uses_allocator = std::uses_allocator<U, policy_based_allocator>::value;
constexpr bool ctr_1 = std::is_constructible<U, Args...>::value;
constexpr bool ctr_2 = std::is_constructible<U, std::allocator_arg_t, policy_based_allocator<U, Policy>, Args...>::value;
constexpr bool ctr_3 = std::is_constructible<U, Args..., policy_based_allocator<U, Policy>>::value;
if constexpr (!uses_allocator && ctr_1)
{
construct_impl(p, std::forward<Args>(args)...);
}
else if constexpr (uses_allocator && ctr_2)
{
construct_impl(p, std::allocator_arg, *this, std::forward<Args>(args)...);
}
else if constexpr (uses_allocator && ctr_3)
{
construct_impl(p, std::forward<Args>(args)..., *this);
}
else
{
static_assert(dependent_false<U>::value, "No valid constructor");
}
}
template<typename U>
void destroy(U * p)
{
if constexpr(trait::has_explicit_stateful_destroy::value)
{
// Stateful destroy
Policy::destroy(base::m_resource, p);
}
else
{
trait::destroy_method::destroy(p);
}
}
void deallocate(T * p, std::size_t n)
{
if constexpr(trait::has_explicit_stateful_deallocate::value)
{
// Stateful deallocate
Policy::deallocate(base::m_resource, p, n);
}
else
{
trait::deallocate_method::deallocate(p, n);
}
}
private:
template<class U, class... Args>
void construct_impl(U * p, Args && ... args)
{
if constexpr(trait::has_explicit_stateful_construct::value)
{
// Stateful construct
Policy::construct(base::m_resource, p, std::forward<Args>(args)...);
}
else
{
trait::construct_method::construct(p, std::forward<Args>(args)...);
}
}
};
template<class T, class U, typename Policy>
bool operator==(const policy_based_allocator<T, Policy> & lhs, const policy_based_allocator<U, Policy> & rhs)
{
if constexpr (policy_based_allocator<T, Policy>::is_always_equal::value)
{
return true;
}
else
{
return lhs.m_resource == rhs.m_resource;
}
}
template<class T, class U, typename Policy>
bool operator!=(const policy_based_allocator<T, Policy> & lhs, const policy_based_allocator<U, Policy> & rhs)
{
return !(lhs == rhs);
}
template<class Alloc>
struct allocator_traits_demo
{
private:
template<class U>
using detect_policy = typename U::policy;
public:
using has_policy = std::experimental::is_detected<detect_policy, Alloc>;
};
//default
template<typename Alloc, typename Enable = void>
struct allocator_delete : Alloc
{
using alloc_traits = std::allocator_traits<Alloc>;
using pointer = typename alloc_traits::pointer;
template<class OtherAlloc>
explicit allocator_delete(OtherAlloc && alloc)
: Alloc{std::forward<OtherAlloc>(alloc)} {}
void operator()(pointer p)
{
//Use allocator
alloc_traits::destroy(*this, p);
alloc_traits::deallocate(*this, p, 1);
};
};
template<class U>
using detect_policy_requires_stateful_deleter = std::negation<typename allocator_policy_traits<typename U::policy>::requires_stateful_deleter>;
template<typename Alloc>
using stateless_deleter_due_to_policy = std::experimental::detected_or_t<std::false_type, detect_policy_requires_stateful_deleter, Alloc>;
template<typename Alloc>
using stateless_deleter_due_to_stateless = std::is_empty<Alloc>;
template<typename Alloc>
using stateless_deleter = std::disjunction<
stateless_deleter_due_to_stateless<Alloc>,
stateless_deleter_due_to_policy<Alloc>
>;
template<typename Alloc>
static constexpr bool stateless_deleter_v = stateless_deleter<Alloc>::value;
//Stateless specialization
template<typename Alloc>
struct allocator_delete<
Alloc,
std::enable_if_t<stateless_deleter_v<Alloc>>
> /*No Alloc storage*/
{
using alloc_traits = std::allocator_traits<Alloc>;
using pointer = typename alloc_traits::pointer;
using value_type = typename alloc_traits::value_type;
template<class OtherAlloc>
explicit allocator_delete(OtherAlloc &&) noexcept {}
void operator()(pointer p)
{
if constexpr (allocator_traits_demo<Alloc>::has_policy::value)
{
using policy = typename Alloc::policy;
using policy_trait = allocator_policy_traits<policy>;
policy_trait::destroy_method::destroy(p);
policy_trait::deallocate_method::deallocate(p, 1);
}
else if constexpr (std::is_empty_v<Alloc>)
{
auto a = Alloc{};
a.destroy(p);
a.deallocate(p, 1);
}
};
};
template<typename T, typename Alloc, typename ... Args>
auto allocate_unique(Alloc const & allocator, Args && ... args)
{
using value_type = std::remove_cv_t<T>; // Clean the type
using alloc_traits = typename std::allocator_traits<Alloc>::
template rebind_traits<value_type>; // Get actual allocator needed
using pointer = typename alloc_traits::pointer;
using allocator_type = typename alloc_traits::allocator_type;
using deleter_T = allocator_delete<allocator_type>; // type of custom delete we will use
auto allocator_T = allocator_type{allocator};
auto mem = alloc_traits::allocate(allocator_T, 1); // will throw is can not allocate
auto hold_deleter = [&](pointer p) { alloc_traits::deallocate(allocator_T, p, 1); }; // Custom deleter
auto holder = std::unique_ptr<value_type, decltype(hold_deleter)>(mem, hold_deleter); // RAII protect mem
auto deleter = deleter_T{allocator_T}; // Make actual custom deleter (may throw) do this before construction
alloc_traits::construct(allocator_T, holder.get(), std::forward<Args>(args)...); // Construct in mem (may throw)
return std::unique_ptr<value_type, deleter_T>(holder.release(),
std::move(deleter)); // repackage with new deleter
}
//*********************USER CODE BELLOW
// allocator resource that owns a slab of memory and allocated from that slab
struct slab_allocator_resource
{
explicit slab_allocator_resource(std::size_t spaceNeeded) :
m_buffer(std::malloc(spaceNeeded)),
m_freeBegin(m_buffer),
m_freeSpace(spaceNeeded)
{
if (!m_buffer) throw std::bad_alloc();
}
~slab_allocator_resource() { std::free(m_buffer); }
template<typename T>
[[nodiscard]] T * allocate(std::size_t n)
{
return static_cast<T *>(allocate_bytes(alignof(T), sizeof(T) * n));
}
private:
void * allocate_bytes(std::size_t align, std::size_t bytes)
{
auto mem = std::align(align, bytes, m_freeBegin, m_freeSpace);
if (!mem) throw std::bad_alloc();
m_freeBegin = static_cast<char *>(m_freeBegin) + bytes;
m_freeSpace -= bytes;
return mem;
}
void * m_buffer;
void * m_freeBegin;
std::size_t m_freeSpace;
};
// Cooperative allocator policy for the slab_allocator_resource
// static methods are important (needed for detection)
struct slab_allocator_resource_policy
{
using resource = slab_allocator_resource;
// explicit stateful allocate
template<typename T>
static T * allocate(resource * s, std::size_t n)
{
return s->allocate<T>(n);
}
// use a common policy method
using deallocate_method = deallocate_as_noop;
};
struct slab_allocator_resource_policy_not_stateless
{
using resource = slab_allocator_resource;
// explicit stateful allocate
template<typename T>
static T * allocate(resource * s, std::size_t n)
{
return s->allocate<T>(n);
}
template<typename T>
static void deallocate(resource * s, T * p, std::size_t n) noexcept
{
//using stateful method signature for illustration purposes only
}
};
template<typename T>
using slab_allocator = policy_based_allocator<T, slab_allocator_resource_policy>;
template<typename T>
using slab_allocator_not_stateless = policy_based_allocator<T, slab_allocator_resource_policy_not_stateless>;
int main()
{
auto res = slab_allocator_resource{1024};
{
auto vec = std::vector<char, slab_allocator<char>>{&res};
vec.reserve(10); //10 bytes used
auto a = vec.get_allocator();
//stateless deleter from policy
auto ptr1 = allocate_unique<char>(a, 'a');
static_assert(sizeof(ptr1) == sizeof(void *));
//stateless deleter from std::allocator
auto ptr2 = allocate_unique<char>(std::allocator<char>{}, 'a');
static_assert(sizeof(ptr2) == sizeof(void *));
}
{
auto a = slab_allocator_not_stateless<char>{&res};
//stateful deleter from policy
auto ptr3 = allocate_unique<char>(a, 'a');
static_assert(sizeof(ptr3) > sizeof(void *));
}
}