What is the idiomatic way to create a fixed size std::array from a fixed size std::span?

357 views Asked by At

I am trying to create a std::array<uint8_t,N> from a std::span<uint8_t,N> but I cannot find a way to do so without memcpy, std::copy, or std::ranges::copy which don't protect me against wrong specification of destination array size.

#include <algorithm>
#include <array>
#include <iostream>
#include <span>

int main(int argc, char **argv) {
  constexpr size_t N = 10;
  std::array<uint8_t, N> original;
  std::span span(original); // of type std::span<uint8,N>

  std::array copy1(span);                               // does not work
  std::array<uint8_t, N> copy2(span);                   // does not work
  std::array<uint8_t, N> copy3(begin(span), end(span)); // does not work


  // ugly stuff that works, but does not protect me if I specify wrong array size
  constexpr size_t M{N - 1}; //oops, leads to array overflow
  std::array<uint8_t, M> copy4;
  std::copy(begin(span), end(span), copy4.begin());
  std::ranges::copy(span, copy4.begin());

  return 0;
}

What is the idiomatic way to do this in modern C++?

3

There are 3 answers

1
Jan Schultke On BEST ANSWER

To expand on @Jarod42's answer, we can make a few improvements:

#include <span>
#include <array>
#include <cstring>
#include <algorithm>

// 1. constrain this function to copy-constructible types
template <std::copy_constructible T, std::size_t N>
    requires (N != std::dynamic_extent)
// 2. handle spans of const/volatile T correctly
std::array<std::remove_cv_t<T>, N> to_array(std::span<T, N> s)
// 3. add conditional noexcept specification
    noexcept(std::is_nothrow_copy_constructible_v<T>)
{
    // add type alias so we don't repeat the return type
    using result_type = decltype(to_array(s));
    if constexpr (std::is_trivial_v<T>) {
        // 4. avoid unnecessary instantiations of std::index_sequence etc.
        //    in the cases where we can copy with no overhead (should be fairly common)
        result_type result;
        // note: we cannot simply use std::memcpy here because it would not
        //       correctly handle volatile T
        std::ranges::copy(s, result.begin());
        return result;
    }
    // TODO: consider using std::ranges::copy for all default-constructible
    //       and copyable types, because the following results in huge assembly output
    else {
        // if 4. is not applicable, we still have to use @Jarod42's solution
        return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
            return result_type{s[Is]...};
        }(std::make_index_sequence<N>{});
    }
}

If you wanted to further reduce the assembly size, you could use the following condition instead:

std::is_default_constructible_v<T> && std::is_copy_assignable_v<T>

If you fear that there is overhead from std::ranges::copy over initialization, you can use std::is_trivially_default_constructible_v<T>, possibly with std::ranges::uninitialized_copy, which should mitigate this.

3
康桓瑋 On

but I cannot find a way to do so without memcpy, std::copy, or std::ranges::copy which don't protect me against wrong specification of destination array size.

If a span has a static extent, its size() can be implemented as a constant expression, which works on current mainstream compilers:

std::array<uint8_t, span.size()> copy4;
std::ranges::copy(span, copy4.begin());

Or you can get the size value through its static member constant extent (like std::array<uint8_t, span.extent>), which is guaranteed to work.

4
Jarod42 On

You might wrap it in a function:

template <typename T, std::size_t N>
std::array<T, N> to_array(std::span</*const*/ T, N> s)
requires (N != std::dynamic_extent)
{
    return [&]<std::size_t... Is>(std::index_sequence<Is...>){
        return std::array<T, N>{{s[Is]...}};
    }(std::make_index_sequence<N>());
}

Note: I avoid std::copy as it requires to have default constructor for T (for the initial array).