c++c++20stdarraystd-span

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


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++?


Solution

  • 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.