Discussion:
[std-proposals] Inferring stateless deleter from a partially stateful allocator
Gareth Lloyd
2018-10-17 14:17:21 UTC
Permalink
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 *));
}

}

Things to note:

- Each method signature dictates if it is stateful or not
- Mechanism to refer to common implementations
- Policy has no state
- Allocator conditionally has state
- Deleter conditionally has state
- Tried to make a policy which is simple to write and an easy facility
to make a good quality allocator (I may have missed stuff in pursuit of
making a "simple" policy)
- allocate_unique can takes a stateful allocator and make a unique_ptr
that has a stateless deleter

I propose:

- something like policy_based_allocator which exposes what parts are
stateful or stateless
- OR another way to do this ....

Requesting thoughts on:

- Everything. I hacked this together a few months ago and cleaned it up
to post here.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CANgTfEhMmPmhgK2B1Lw4vJCDN9fUJ9mUWaL_mDNx6OBLXCWN7w%40mail.gmail.com.
Arthur O'Dwyer
2018-10-19 15:40:01 UTC
Permalink
Post by Gareth Lloyd
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.
IIUC and AFAICT, the only situation where this comes up in practice is with
memory resources (heaps) where `deallocate` is a no-op.

In practice `destroy` is always "stateless" because its behavior depends
only on the type T being destroyed, not on the identity of the allocator
doing the destruction. (In fact, I think Mark Zeren is working on a
proposal to deprecate `destroy`, although I don't see it in the pre–San
Diego mailing.)
In practice, `deallocate` is frequently "stateless," e.g. when the
allocator is std::allocator<T>; but in most cases `deallocate` is stateless
*because* the allocator is stateless (i.e. the allocator has only one
possible value), in which case your optimization doesn't buy us anything
because the allocator is already an empty class. So the usefulness is
limited (in practice, AFAICT) to the case where the allocator is stateful
(or, as I like to say
"value-ful") and
where `deallocate` is a no-op.

`deallocate` is not a no-op for any standard-library-provided allocator
type (std::allocator, std::pmr::polymorphic_allocator,
std::scoped_allocator_adaptor).
It could conceivably be a no-op for a user-provided allocator type A<T>
where A<T>'s value was restricted to point to a memory resource with a
no-op `deallocate`. For example:

template<class T>
struct monotonic_buffer_allocator : public
std::pmr::polymorphic_allocator<T> {
using Base = std::pmr::polymorphic_allocator<T>;

monotonic_buffer_allocator(std::pmr::monotonic_buffer_resource *mr) :
mr_(mr) {}
template<class U> monotonic_buffer_allocator(const
monotonic_buffer_allocator<U>& rhs) : Base(rhs) {}

using Base::allocate;
using Base::construct;
using Base::destroy;
void deallocate(T *, size_t) {}

template<class U> struct rebind { using other =
monotonic_buffer_allocator<U>; }
};

`monotonic_buffer_allocator` here is "value-ful" (non-empty) but also has a
no-op `deallocate` and a stateless `destroy`; so conceivably
`allocate_unique` could be written to take advantage of this property.

I would spell it `std::has_trivial_deallocate_v<A<T>>`.
Incidentally, I don't like your `stateless_deleter_v<A<T>>` because it is
missing an `is_` on the front.

Furthermore, observe that when `has_trivial_deallocate_v<A<T>>`, then the
`unique_ptr` returned from `allocate_unique<T, A<T>>` can be *trivially
destructible*. This seems like a very nice property to have,
philosophically speaking, even though I can't off the top of my head come
up with any concrete application for it.

A compromise approach might be to permit the allocator to specify a member
typedef `using deleter = ...` (defaulted to `allocator_delete<A<T>>` in
`allocator_traits`) which could be customized by the designer of the
allocator type. Then this deleter could be stateless and/or have a trivial
`deallocate` method.

Bottom lines:
- I think the property of *trivial* deallocate is much more directly
relevant to your use-case than is the property of *stateless* deallocate.
- I think it is not worth asking WG21 to pursue the *detection and
exploitation* of either property, until there *exists* some standard
allocator having the property, which is not likely ever to happen. It's
easy to hack up the existence and detection mechanisms within your own
codebase, as you've done, and then provide `my::allocate_unique` to exploit
them. You don't need to be friends with `std::unique_ptr` to make that
happen; you just need to provide a custom deleter type, which is already
possible in standard C++.

–Arthur
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/5f11127d-1ab8-41d8-8b8d-8e9781770f20%40isocpp.org.
Gareth Lloyd
2018-10-23 00:17:12 UTC
Permalink
Post by Arthur O'Dwyer
IIUC and AFAICT, the only situation where this comes up in practice is
with memory resources (heaps) where `deallocate` is a no-op.
In practice `destroy` is always "stateless" because its behavior depends
only on the type T being destroyed, not on the identity of the allocator
doing the destruction. (In fact, I think Mark Zeren is working on a
proposal to deprecate `destroy`, although I don't see it in the pre–San
Diego mailing.)
I can't think of a useful customization of `destroy` but one may exist, I'd
like to hear more on the idea of deprecating it.
Post by Arthur O'Dwyer
Furthermore, observe that when `has_trivial_deallocate_v<A<T>>`, then the
`unique_ptr` returned from `allocate_unique<T, A<T>>` can be *trivially
destructible*. This seems like a very nice property to have,
philosophically speaking, even though I can't off the top of my head come
up with any concrete application for it.
`unique_ptr` itself would need specializing to make it actually trivially
destructibe and not just noop destructor. value_type must be trivailly
destructible, allocator uses default destroy and noop deallocate. (On a
related note see
https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/Rx3EmEs5Isw)
Post by Arthur O'Dwyer
A compromise approach might be to permit the allocator to specify a member
typedef `using deleter = ...` (defaulted to `allocator_delete<A<T>>` in
`allocator_traits`) which could be customized by the designer of the
allocator type. Then this deleter could be stateless and/or have a trivial
`deallocate` method.
This possibly hints that a dual concept of a `maker` to mirror that of the
`deleter`. Those would be a convient allocator_trait additions to have.
Post by Arthur O'Dwyer
- I think the property of *trivial* deallocate is much more directly
relevant to your use-case than is the property of *stateless* deallocate.
I think both are important.

- I think it is not worth asking WG21 to pursue the *detection and
Post by Arthur O'Dwyer
exploitation* of either property, until there *exists* some standard
allocator having the property, which is not likely ever to happen. It's
easy to hack up the existence and detection mechanisms within your own
codebase, as you've done, and then provide `my::allocate_unique` to exploit
them. You don't need to be friends with `std::unique_ptr` to make that
happen; you just need to provide a custom deleter type, which is already
possible in standard C++.
Custom deleter only gives us the noop deallocation, trivial destruction of
unique_ptr requires more.
Should P0316R0 "allocate_unique and allocator_delete" only consider the
standard allocator? If it makes it into that standard then it will need to
allow specialized allocator_delete?
In my other post
(https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/Rx3EmEs5Isw)
I show limitations of the winked-out optimization with existing containers
and existing allocators. This may motivate the need for either more
allocators in the standard or better facilities to make your own.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/abd7a3bc-e438-4fa3-b26a-a8888bfec11a%40isocpp.org.
Gareth Lloyd
2018-10-23 12:36:35 UTC
Permalink
Post by Arthur O'Dwyer
A compromise approach might be to permit the allocator to specify a member
Post by Arthur O'Dwyer
typedef `using deleter = ...` (defaulted to `allocator_delete<A<T>>` in
`allocator_traits`) which could be customized by the designer of the
allocator type. Then this deleter could be stateless and/or have a trivial
`deallocate` method.
This possibly hints that a dual concept of a `maker` to mirror that of the
`deleter`. Those would be a convient allocator_trait additions to have.
Having slept on this the obvious concept name would be `newer` (not a fan
of the name). User defined types can provide new/delete overrides
(customization point for external allocation). I know their are good
reasons why allocators (customization point for internal allocations) did
not go with the newer/deleter paradigm, there are advantages to working
with raw memory separately from the object materialization(s). The
newer/deleter typedefs do add convenience to the type designers that use
allocators, but for the winked-out optimization giving a trivial
destructible type `is_trivial_deleter_v` would also need to be part of the
allocator (optional) + allocator_trait. That or some canonical
implementation of the noop_deleter, that by convention is_same<> can be
used to enable the is_trivial_destructible<> on the generic type (I do not
advocate this approach).
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/5474c7c2-f539-4b53-85df-4f7449b8c285%40isocpp.org.
Bryce Adelstein Lelbach aka wash
2018-10-24 07:04:44 UTC
Permalink
An example of a weird destroy: the object lives on an accelerator and needs
to be destroyed by a thread local to the accelerator.
Post by Arthur O'Dwyer
IIUC and AFAICT, the only situation where this comes up in practice is
Post by Arthur O'Dwyer
with memory resources (heaps) where `deallocate` is a no-op.
In practice `destroy` is always "stateless" because its behavior depends
only on the type T being destroyed, not on the identity of the allocator
doing the destruction. (In fact, I think Mark Zeren is working on a
proposal to deprecate `destroy`, although I don't see it in the pre–San
Diego mailing.)
I can't think of a useful customization of `destroy` but one may exist,
I'd like to hear more on the idea of deprecating it.
Post by Arthur O'Dwyer
Furthermore, observe that when `has_trivial_deallocate_v<A<T>>`, then the
`unique_ptr` returned from `allocate_unique<T, A<T>>` can be *trivially
destructible*. This seems like a very nice property to have,
philosophically speaking, even though I can't off the top of my head come
up with any concrete application for it.
`unique_ptr` itself would need specializing to make it actually trivially
destructibe and not just noop destructor. value_type must be trivailly
destructible, allocator uses default destroy and noop deallocate. (On a
related note see
https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/Rx3EmEs5Isw
)
Post by Arthur O'Dwyer
A compromise approach might be to permit the allocator to specify a
member typedef `using deleter = ...` (defaulted to `allocator_delete<A<T>>`
in `allocator_traits`) which could be customized by the designer of the
allocator type. Then this deleter could be stateless and/or have a trivial
`deallocate` method.
This possibly hints that a dual concept of a `maker` to mirror that of the
`deleter`. Those would be a convient allocator_trait additions to have.
Post by Arthur O'Dwyer
- I think the property of *trivial* deallocate is much more directly
relevant to your use-case than is the property of *stateless* deallocate.
I think both are important.
- I think it is not worth asking WG21 to pursue the *detection and
Post by Arthur O'Dwyer
exploitation* of either property, until there *exists* some standard
allocator having the property, which is not likely ever to happen. It's
easy to hack up the existence and detection mechanisms within your own
codebase, as you've done, and then provide `my::allocate_unique` to exploit
them. You don't need to be friends with `std::unique_ptr` to make that
happen; you just need to provide a custom deleter type, which is already
possible in standard C++.
Custom deleter only gives us the noop deallocation, trivial destruction of
unique_ptr requires more.
Should P0316R0 "allocate_unique and allocator_delete" only consider the
standard allocator? If it makes it into that standard then it will need to
allow specialized allocator_delete?
In my other post (
https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/Rx3EmEs5Isw)
I show limitations of the winked-out optimization with existing containers
and existing allocators. This may motivate the need for either more
allocators in the standard or better facilities to make your own.
--
You received this message because you are subscribed to the Google Groups
"ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an
To view this discussion on the web visit
https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/abd7a3bc-e438-4fa3-b26a-a8888bfec11a%40isocpp.org
<https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/abd7a3bc-e438-4fa3-b26a-a8888bfec11a%40isocpp.org?utm_medium=email&utm_source=footer>
.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CAP3wax_tk%2B_%3DGiVrsCUFrDvzV08OOcpMkK7j%3Dk_UfhZmAVCuiA%40mail.gmail.com.
Arthur O'Dwyer
2018-10-27 03:07:21 UTC
Permalink
Post by Arthur O'Dwyer
IIUC and AFAICT, the only situation where this comes up in practice is
Post by Arthur O'Dwyer
with memory resources (heaps) where `deallocate` is a no-op.
In practice `destroy` is always "stateless" because its behavior depends
only on the type T being destroyed, not on the identity of the allocator
doing the destruction. (In fact, I think Mark Zeren is working on a
proposal to deprecate `destroy`, although I don't see it in the pre–San
Diego mailing.)
I can't think of a useful customization of `destroy` but one may exist,
I'd like to hear more on the idea of deprecating it.
Post by Arthur O'Dwyer
Furthermore, observe that when `has_trivial_deallocate_v<A<T>>`, then the
`unique_ptr` returned from `allocate_unique<T, A<T>>` can be *trivially
destructible*. This seems like a very nice property to have,
philosophically speaking, even though I can't off the top of my head come
up with any concrete application for it.
`unique_ptr` itself would need specializing to make it actually trivially
destructible and not just noop destructor.
Right. This is the only reason (AFAICT) why you'd desire cooperation from
the standard library. Everything else you want to do — everything *except*
make unique_ptr trivially destructible — can be done inside
your::allocate_unique() in plain vanilla C++11 with no cooperation from the
standard library at all.
Post by Arthur O'Dwyer
value_type must be trivially destructible, allocator uses default destroy
and noop deallocate. (On a related note see
https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/Rx3EmEs5Isw
)
For the former, std::is_trivially_destructible<ValueType> is provided by
the standard library.
For the latter two, your::allocate_unique() would have to examine
your::has_noop_deallocate_method<Allocator>, but that's fine because no
*standard* allocators have noop `deallocate` methods, so you don't have to
cooperate with the standard library for that one.

–Arthur
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CADvuK0LocKvswhVB%2BiAtNCGazpJPKFwgMv0thmA8T_u%2BxETbYA%40mail.gmail.com.
Gareth Lloyd
2018-10-29 22:52:47 UTC
Permalink
Furthermore, observe that when `has_trivial_deallocate_v<A<T>>`, then the
Post by Arthur O'Dwyer
Post by Gareth Lloyd
Post by Arthur O'Dwyer
`unique_ptr` returned from `allocate_unique<T, A<T>>` can be *trivially
destructible*. This seems like a very nice property to have,
philosophically speaking, even though I can't off the top of my head come
up with any concrete application for it.
`unique_ptr` itself would need specializing to make it actually trivially
destructible and not just noop destructor.
Right. This is the only reason (AFAICT) why you'd desire cooperation from
the standard library. Everything else you want to do — everything *except*
make unique_ptr trivially destructible — can be done inside
your::allocate_unique() in plain vanilla C++11 with no cooperation from the
standard library at all.
Post by Gareth Lloyd
value_type must be trivially destructible, allocator uses default destroy
and noop deallocate. (On a related note see
https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/Rx3EmEs5Isw
)
For the former, std::is_trivially_destructible<ValueType> is provided by
the standard library.
For the latter two, your::allocate_unique() would have to examine
your::has_noop_deallocate_method<Allocator>, but that's fine because no
*standard* allocators have noop `deallocate` methods, so you don't have
to cooperate with the standard library for that one.
I urge that you read '“Winked-out” optimization on generic collections"'
https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/Rx3EmEs5Isw
if you have not already. It should help frame my motivations better.

It is not just unique_ptr this applies to, I have considering more that
that. I desire such a facility within the standard library because
something like this is required to enable all types in the standard, that
use allocators, to have a standard mechanism to be trivial_destructible
depending upon the provided allocator.

These posts are based on a code transformation process, based on John Lakos
ACCU and CppCon talks, that I've done on the codebase I work on. Using the
PMR pattern with monotonic_buffer_resource lost the winked out optimization
(LTO didn't help) and trivial destructable types that the codebase
previously had. I did this work because of the shortcomings I found with
using std::pmr::monotonic_buffer_resource with generic code. This pattern
allowed me to decouple the allocator used (using regular allocator concept)
which helps with testing a component, but in production still have an
optimized datastructure with the more optimizable monotonic allocator given
via template parameter. My findings were that current set of standard
allocators are not complete/finished, pmr has demonstrated one way to do
allocators but it is not nessisarily the holy grale or the end.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CANgTfEhvTfauen%3DGS71y11T%2BQY48Z7joDNXB5O5T%3D8LpYNgTJg%40mail.gmail.com.
Loading...