1
\$\begingroup\$

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 inherit span, 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.
\$\endgroup\$
13
  • 3
    \$\begingroup\$ The name is a contradiction: unique⇒"owning" and span⇒"non-owning", so I dislike that already! When I need an owner of run-time-sized array, I go straight to std::vector. The ability to resize doesn't cost anything if you don't use it... \$\endgroup\$ Commented Aug 4 at 12:49
  • \$\begingroup\$ “I made unique_span inherit span, 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\$
    – indi
    Commented Aug 4 at 13:23
  • \$\begingroup\$ @indi: Explanation is at the link, but basically, otherwise, I can't pass unique_span to a template <typename T> foo(span<T>) - despite the unique_span's conversion operator. overload resolution fails. \$\endgroup\$
    – einpoklum
    Commented Aug 4 at 14:25
  • 1
    \$\begingroup\$ @TobySpeight: The English word "span" in itself does not suggest ownership or non-ownership of what's spanned (correct me if I'm wrong). So, the idiomatic use in C++ is span for non-owning and I'll add unique span for ownership. Just like we have pointers - which may or may not be owning - and "unique pointers". But if you have a better suggestion, I could still change the name. \$\endgroup\$
    – einpoklum
    Commented Aug 4 at 14:26
  • 1
    \$\begingroup\$ Could you include in the description a short summary of what this provides that 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\$ Commented Aug 4 at 16:22

1 Answer 1

3
\$\begingroup\$

Code presented is uncompilable

293195.cpp:8:23: error: ‘span’ does not name a type
    8 |     using span_type = span<T>;
      |                       ^~~~
293195.cpp:8:23: note: (perhaps ‘typename std::span<_Type, 18446744073709551615>::span’ was intended)
293195.cpp:12:32: error: expected nested-name-specifier before ‘span’
   12 |     using size_type = typename span<T>::size_type;
      |                                ^~~~
293195.cpp:13:30: error: expected nested-name-specifier before ‘span’
   13 |     using pointer = typename span<T>::pointer;
      |                              ^~~~
293195.cpp:14:32: error: expected nested-name-specifier before ‘span’
   14 |     using reference = typename span<T>::reference;
      |                                ^~~~
293195.cpp:17:11: error: expected nested-name-specifier before ‘span’
   17 |     using span<T>::data;
      |           ^~~~
293195.cpp:18:11: error: expected nested-name-specifier before ‘span’
   18 |     using span<T>::size;
      |           ^~~~
293195.cpp: In destructor ‘unique_span<T, Deleter>::~unique_span()’:
293195.cpp:56:13: error: there are no arguments to ‘data’ that depend on a template parameter, so a declaration of ‘data’ must be available [-fpermissive]
   56 |         if (data() != nullptr) {
      |             ^~~~
293195.cpp:56:13: note: (if you use ‘-fpermissive’, G++ will accept your code, but allowing the use of an undeclared name is deprecated)
293195.cpp:57:28: error: there are no arguments to ‘data’ that depend on a template parameter, so a declaration of ‘data’ must be available [-fpermissive]
   57 |             deleter_type{}(data());
      |                            ^~~~

plus several more consequential on the invalidity of span_type. You should fix this before proceeding further. Suggested fix:

public:
    using span_type = std::span<T>;

    // Exposing some span type definitions, strictly for terseness
    // (they're all visible on the outside anyway)
    using size_type = typename span_type::size_type;
    using pointer = typename span_type::pointer;
    using reference = typename span_type::reference;
    using deleter_type = Deleter;

    using span_type::data;
    using span_type::size;

Inheriting from std types is inadvisable

Standard library types are not intended for use as bases for inheritance. Notably, they are not specified with virtual destructors.

A particular risk is that a user might assign through a reference to the base class - the consequences might "only" be a memory leak, but could extend to UB if we attempt to double-delete or to delete storage not obtained from new[] (adjust as appropriate for non-default deleters).

It's better to provide a type from which a std::span can be derived, as std::vector does.

Some overcomplications

Move-assignment would be simpler using swap():

    unique_span& operator=(unique_span&& other) noexcept
    {
        swap(other);
        return *this;
    }

We only need to delete the defaultable copy constructor. There's no need for this template which wouldn't be generated if not mentioned:

template<typename U, typename OtherDeleter>
unique_span(const unique_span<U, OtherDeleter>&) = delete;

This is a long-winded way to assign to the base-class:

        static_cast<span_type&>(*this) = span_type{static_cast<T*>(nullptr), 0};

Consider rewriting as span_type::operator=({});.

Missing conversions

We should be able to convert from a span of T to a span of const T:

    auto x = make_unique_span<int>(15);
    unique_span<const int> y{std::move(x)};

This isn't possible with the current implementation. We could replace the move-constructors with one that allows cv conversions:

    template<typename U, typename UDel>
    requires std::is_assignable_v<span_type, std::span<U>>
          && std::is_assignable_v<Deleter, UDel>
    unique_span(unique_span<U,UDel>&& other) noexcept
         : unique_span{ other.release() }
    {
    }

N.B. we don't know that release() is noexcept, because (perhaps surprisingly) deleters don't promise not to throw (not even std::default_deleter). So making any of our constructors noexcept is a leap of faith.

Deleter is not stored

Code assumes a default-constructed deleter is the same as the passed deleter. That's not a safe assumption, which is why allocator-aware code uses is_always_equal to determine whether the allocator needs to be stored.

Factory method is not allocator-aware

make_unique_span() always uses global new[] and delete[], but could easily accept an optional allocator argument.

Factory method requires default-constructible element type

make_unique_span() has no option to provide a value to initialise elements with.

No tests

The unit tests appear to be completely omitted from the review.


Recommendation

Use a std::unique_ptr<T[]> as storage and remember the length:

template<class T, class Deleter>
class unique_span
{
     std::unique_ptr<T[], Deleter> data;
     std::size_t length;
};

Then copy/move is all done for us, and we can easily convert to span by adding an appropriate operator member:

    operator std::span<T>() const&;

The resultant class:

template<typename T, typename Deleter = ::std::default_delete<T[]>>
class unique_span
{
    std::unique_ptr<T[], Deleter> data = {};
    std::size_t length = 0;

    template<typename U, typename UDel>
    friend class unique_span;

public:
    unique_span() = default;
    explicit unique_span(std::size_t size)
        requires std::is_default_constructible_v<T>
        : data{std::make_unique<T[]>(size)},
          length{size}
    { }
    explicit unique_span(T *data, std::size_t size, Deleter del = {}) noexcept
        : data{data, del},
          length{size}
    { }
    explicit unique_span(std::span<T> span, Deleter del = {}) noexcept
        : data{span.data(), del},
          length{span.size()}
    { }

    template<typename U, typename UDel>
    requires std::is_assignable_v<std::span<T>, std::span<U>>
          && std::is_assignable_v<Deleter, UDel>
    unique_span(unique_span<U,UDel>&& other)
        : data{std::move(other.data)},
          length{other.length}
    {
        other.length = 0;
    }

    operator std::span<T>() const&
    {
        return {data.get(), length};
    }
};

I think you're allowed to provide a deduction guide (because it depends on at least one user-defined type):

template<typename T, typename Deleter>
std::span(unique_span<T,Deleter>) -> std::span<T>;

That allows you to call functions accepting std::span<T> in the same style that you would if you had some other contiguous range object such as std::vector. E.g. given this function:

template<typename T> void foo(std::span<T>)

Then we can call it:

    auto x = unique_span<int>(15);
    foo(std::span{x});

Left as an exercise: handle spans with non-default Extent, avoiding the need for the length member in such cases.

Also left as an exercise: provide direct accessors such as operator[]. These will generally just forward to the data member. Implementing begin(), end() and data() might be enough to make the type satisfy the contiguous-range requirements for std::span construction without needing a deduction guide.

\$\endgroup\$
16
  • 1
    \$\begingroup\$ I’d second that recommendation, and even add that maybe it should also have an Extent template parameter. And if that’s not std::dynamic_extent, then you don’t even need to store the length. I mean, if just the extra capacity member in vector is enough to make it monstrous, why not avoid the length member if possible, right? I would also suggest lvalue-ref-qualifying the conversion to span, so you don’t accidentally get a view of an expiring array. \$\endgroup\$
    – indi
    Commented Aug 4 at 21:40
  • \$\begingroup\$ The recommendation fails to meet my usability criterion, of being usable with functions such as template <T> foo(span<T>). The conversion operator would not be considered when trying to compile foo(instance_of_the_recommended_unique_span); IIANM. But thank you for the rest of your suggestions. \$\endgroup\$
    – einpoklum
    Commented Aug 5 at 0:02
  • 1
    \$\begingroup\$ As for the conversion to span, you don’t need a deduction guide or a conversion operator; just make unique_span a valid range (maybe you just need begin() & end()), and span has a constructor that will work (implicit when the extent is dynamic, too). godbolt.org/z/To9TvjYME \$\endgroup\$
    – indi
    Commented Aug 5 at 13:57
  • 1
    \$\begingroup\$ “It can't be passed to function templates taking a span, though: godbolt.org/z/ehK946Ys9” Anyone who writes a function template like that doesn’t know what they are doing. Nothing will implicitly work with that that is not a span. It makes no sense to write a function template like that in any case. Your requirements are impossible to meet because they are nonsensical. It makes no sense to design something to work with broken code, rather than just fixing the broken code. \$\endgroup\$
    – indi
    Commented Aug 6 at 21:06
  • 1
    \$\begingroup\$ Credit to Anna for reminding me of another reason that templated span pattern is so terrible: sometimes even spans themselves will not implicitly work. godbolt.org/z/chEcY151d \$\endgroup\$
    – indi
    Commented Aug 9 at 21:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.