After nearly 30 years of C++ development, you’d think the little papercuts would stop bothering me. But sometimes you hit one that’s so perfectly illustrative of the language’s occasional blind spots that it demands attention.

Today’s frustration: std::array<T, N> lacks an assignment operator that lets you pass in a std::span<T, N>. It’s a small thing, but it is incredibly frustrating.

The Problem

Consider this perfectly reasonable code:

class C
{
    std::array<unsigned char, 32> data_;

public:
    ...

    void assign(std::span<unsigned char, 32> data)
    {
        data_ = data; // Error!
    }
};

We have two containers with the same element type and the same compile-time size: a perfect match. Yet there’s no ability to do the “natural” thing. And this is surprising.

The Proposed Solution

So why not add assignment operators to std::array, allowing for easy copying of fixed-size spans to an array of the same type and size, enabling the “natural” syntax?

template<class T, std::size_t N>
class array {
    // ... existing members ...
    
    // Proposed addition:
    constexpr array& operator=(std::span<const T, N> other) noexcept(/* appropriate condition */) {
        std::ranges::copy(other, begin());
        return *this;
    }
    
    // For mutable spans too:
    constexpr array& operator=(std::span<T, N> other) noexcept(/* appropriate condition */) {
        std::ranges::copy(other, begin());
        return *this;
    }
};

The Current Workarounds

Right now, we have several verbose and potentially error-prone alternatives:

// Option 1: ranges::copy (verbose)
std::ranges::copy(data_, data);

// Option 2: Manual loop (even more verbose)
for (size_t i = 0; i < data.size(); ++i) {
    data_[i] = data[i];
}

// Option 3: std::copy (C++98 style)
std::copy(data.begin(), data.end(), data_.begin());

Why This Matters

These workarounds fail to express intent as clearly as direct assignment would in this case. But this isn’t just about convenience and expressiveness. It’s also about safety:

The first option, using ranges::copy, has the arguments in the wrong order. As written, std::ranges::copy(data_, data) copies FROM the class’s internal buffer TO the span passed into the function. While this can be avoided by being meticulous about using std::span<const T, N> and the proposed change wouldn’t prevent the programmer from accidentally typing data = data_, it seems that the latter is easier to spot.

The second and third options neither check nor enforce the compile-time size relationship between the array and the span, which is brittle.

Construction from Spans

For the construction case, we could extend std::to_array to accept fixed-size spans:

template<class T, std::size_t N>
requires(N != std::dynamic_extent)
constexpr std::array<std::remove_cv_t<T>, N> to_array(std::span<T, N> s) {
    std::array<std::remove_cv_t<T>, N> result;
    result = s;  // Uses assignment operator
    return result;
}

This would enable the natural syntax:

template <size_t N>
auto twiddle(std::span<const std::uint64_t, N> dee)
{
    auto dum = std::to_array(dee);

    for (auto& d : dum)
        d ^= 0xDEADBEEF;

    return dum;
}

This approach follows the existing pattern of std::to_array as the utility function for creating arrays from other containers, while avoiding any issues with aggregate initialization that adding constructors to std::array would cause.

It Makes Sense

This addition makes sense and feels like a perfect candidate for a standard library enhancement:

  1. It is non-breaking: no existing code could break, since any code that includes this syntax would not compile without this change.
  2. It provides compile-time safety: the type of the array and the span must be same-ish, upto const and the sizes must match.
  3. It is a zero-cost and optimally performant abstraction: no runtime checks are needed and the code is as efficient as possible.
  4. It is consistent and expressive: the intention is crystal clear: this is an assignment, and the semantics of the operation are aligned with what we expect from assignment.
  5. It is simple to implement: it only requires the addition of two simple assignment operators to std::array which invoke std::ranges::copy, and a single free function that invokes the new assignment operator. The only consideration is making sure that the proper noexcept specifications are set.

And sometimes these small and simple changes, which naturally become part of our everyday toolbox and fade into the background, are the best ones.