Why is default construction disallowed in std::span with static extent?

Viewed 425

Take a look at this example:

#include <span>
#include <vector>

class Data
{
public:
    Data() = default;
  
    template<class R>
    explicit Data(R& r)
        : buf_(r) 
        , header_(buf_.first<4>())
    {}

private:
    std::span<char> buf_;

    // compile error
    // std::span<char, 4> header_;

    // compiles but ill-formed (against the precondition of [span.sub])
    std::span<char, 4> header_{buf_.first<4>()};
};

int main()
{
    std::vector<char> buf(1234);
    Data data{buf};
}

The reason of compile error is because it is explicitly disallowed in the standard [span.cons]:

constexpr span() noexcept;

Constraints: Extent == dynamic_­extent || Extent == 0 is true.
Postconditions: size() == 0 && data() == nullptr.

Why does this constraint exist?

Since default construction for a span with dynamic_extent is already allowed, it feels like these two cases are semantically same:

std::span<char> header_; // allowed
std::span<char, 4> header_; // why disallowed?

Furthermore, take a look at the wording of [span.obs]:

constexpr size_type size() const noexcept;

Effects: Equivalent to: return size_­;

Let's say for example, the standard could even define stricter implementation details to encourage static optimization. If this wording was something like Returns Extent when Extent != dynamic_extent, the size_ data member can be omitted in runtime. If so, I understand that the default construction is invalid, because in that case size() will always return invalid size, i.e. Extent, which is not zero.

However, current standard exposes the non-static member variables in the class definition [span.overview]:

  private:
    pointer data_;              // exposition only
    size_type size_;            // exposition only

Since we already have those variables, can't the standard library just set data_ = nullptr and size_ = 0 when statically sized span is default constructed? I surely can live with the current wording, but isn't the current standard expecting too strong constraints?

Note that the committee have once attempted to fix [span.cons] already in LWG3198, so I'm pretty sure that they have some rationale for current wording.

3 Answers

A default-constructed std::span<T> successfully refers to 0 objects of type T starting at nullptr. To what 3 objects of type T does std::span<T,3>() refer?

The size_ member always being present is purely a narrative device to simplify the specification; it means nothing and really isn’t there in practice for static-extent spans.

Why does this constraint exist?

Because that's what it means to ask for or specify the size of a span. If you are given a span, any span, the expectation is that span::size will return the number of elements in the array. Period.

If you create a span with X elements in it, you are required to provide a pointer range that actually stores X elements in it. You may of course lie to span, passing it a pointer to fewer elements. But you are breaking your part of the contract; you said that there were X elements in the array, and you provided fewer than X. The UB that results is on you.

It doesn't matter if X is a runtime or compile-time value: the requirement is the same.

To default-construct a statically-sized span is to allow the user to lie by default. A default-initialized span<T, 3> is lying to every user that gets one. It claims to have 3 elements, but it does not.

Good APIs do not let users lie to it by default.

I tend to write this kind of code when no other solution is available:

inline static std::array<char, 4> dummy_header_v;
std::span<char, 4> header_{dummy_header_v};

Personally, this is just a nasty workaround and I really don't like it.

However, there is some benefit for this workaround: the user can expect compiler optimization (such as loop unrolling) whenever one reads from Data::header_, since it is statically sized span std::span<T, N> in the first place. This is useful when you already know the partial buffer size by definition, and when you're going to fetch the real buffer lazily. If the span was declared like std::span<T>, no optimization will ever happen.

If the spec allow default construction, then there's no need of this workaround.

I posted this workaround just for reference; I really appreciate other proper solutions.

Related