Introduction / Motivation
I like the CADRe (a.k.a. RAII) resource management idiom in C++. And I like std::span
's; or perhaps I should say, I dislike the use of pointers to the beginning of (typed or untyped) regions of memory, where one can easily forget how many elements there are. And for this reason, I also dislike std::unique_ptr
's, even though I use them quite frequently: You create them - and they immediately hide the allocation size from you. And if you use a unique_ptr<T[]>
, you often find yourself also holding a span<T>
to actually put it to use as a standard library container (or alternatively, you have to use raw loops, magically remember the allocated size etc.)
So, I ended up combining the two somewhat, creating a unique_ptr
- that also has a size; or a span
- which is also owning. Hence - a unique_span
.
The unique_span
class template
#include <span>
#include <type_traits>
#include <memory>
template<typename T, typename Deleter = ::std::default_delete<T[]>>
class unique_span : public std::span<T> {
public:
using span_type = span<T>;
// Exposing some span type definitions, strictly for terseness
// (they're all visible on the outside anyway)
using size_type = typename span<T>::size_type;
using pointer = typename span<T>::pointer;
using reference = typename span<T>::reference;
using deleter_type = Deleter;
using span<T>::data;
using span<T>::size;
public:
constexpr unique_span() noexcept = default;
// Disable copy construction - as this class never allocates;
unique_span(const unique_span&) = delete;
// ... and also match other kinds of unique_span's, which may get converted into
// a span and thus leak memory on construction!
template<typename U, typename OtherDeleter>
unique_span(const unique_span<U, OtherDeleter>&) = delete;
/// Take ownership of an existing span
///
/// @note These ctors are all explicit to prevent accidentally assuming ownership
/// of a non-owned span when passing to a function, then trying to release that
/// memory returning from it.
///@{
explicit unique_span(span_type span) noexcept : span_type{span} { }
explicit unique_span(pointer data, size_type size) noexcept : unique_span{span_type{data, size}} { }
///@}
/** A move constructor.
*
* @note Moving is the only way a unique_span may have its @ref data_ field become
* null; the user is strongly assumed not to use the `unique_span` after moving from
* it.
*/
unique_span(unique_span&& other) noexcept : unique_span{ other.release() } { }
/// Prevent construction of one kind of unique_span by another kind
template<typename U, typename OtherDeleter>
unique_span(unique_span<U, OtherDeleter>&&) = delete;
~unique_span() noexcept
{
if (data() != nullptr) {
deleter_type{}(data());
}
#ifndef NDEBUG
static_cast<span_type&>(*this) = span_type{static_cast<T*>(nullptr), 0};
#endif
}
public:
/// No copy-assignment - that would break our ownership guarantee
unique_span& operator=(const unique_span&) = delete;
/// A Move-assignment operator, which takes ownership of the other region
unique_span& operator=(unique_span&& other) noexcept
{
span_type released = other.release();
if (data() != nullptr) {
deleter_type{}(data());
}
static_cast<span_type&>(*this) = released;
return *this;
}
/// No plain dereferencing - as there is no guarantee that any object has been
/// initialized at those locations, nor do we know its type
constexpr span_type get() const noexcept { return { data(), size() }; }
/// Exchange the pointer and deleter with another object.
void swap(unique_span& other) noexcept
{
::std::swap<span_type>(*this, other);
}
protected:
/**
* Release ownership of the stored span
*
* @note This is not marked nodiscard by the same argument as for std::unique_ptr;
* see also @url https://stackoverflow.com/q/60535399/1593077 and
* @url http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0600r1.pdf
*/
span_type release() noexcept
{
span_type released { data(), size() };
static_cast<span_type &>(*this) = span_type{static_cast<T*>(nullptr), 0};
return released;
}
}; // class unique_span
/**
* A parallel of ::std::make_unique_for_overwrite, for @ref unique_span<T>'s, i.e. which maintains
* the number of elements allocated.
*
* @tparam T the type of elements in the allocated @ref unique_span.
*
* @param size The number of @tparam T elements to allocate
*/
template <typename T>
unique_span<T> make_unique_span(size_t size)
{
return unique_span<T>{ new T[size], size };
}
Any comments, critique or suggestions are welcome. More specifically though, some design dilemmas I've had are:
- Is it important/useful enough I take a run-time deleter argument, rather than just a (compile-time-)fixed deleter functor? Like
std::unique_ptr
? - I made
unique_span
inheritspan
, because otherwise it is difficult/impossible to use it as a drop-in for span's in functions templated on the span and unique-span's element type. But, this has a price: The class now violates the Liskov Substitution Principle. This particular dilemma was discussed here, but you're welcome chime in on that as well.
unique
⇒"owning" andspan
⇒"non-owning", so I dislike that already! When I need an owner of run-time-sized array, I go straight tostd::vector
. The ability to resize doesn't cost anything if you don't use it... \$\endgroup\$unique_span
inheritspan
, because otherwise it is difficult/impossible to use it as a drop-in for span's in functions templated on the span and unique-span's element type.” What does this mean in plain English? \$\endgroup\$unique_span
to atemplate <typename T> foo(span<T>)
- despite theunique_span
's conversion operator. overload resolution fails. \$\endgroup\$std::vector
doesn't? Vectors implicitly convert to spans, so I don't understand why this class is useful. I'd like that understanding before I start to review. \$\endgroup\$