3

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

phinz
  • 1,225
  • 10
  • 21

3 Answers3

6

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.

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • What would be the way to get the extent from the type of the `span` variable? Like for the array I can write `std::tuple_size_v`, how do I retrieve the extent from `decltype(span)`? – phinz Jul 28 '23 at 18:39
  • 2
    @phinz `span` has a public static member constant named `extent`, which is the value of the template parameter `Extent`, so you can `decltype(span)::extent` or just `span.extent`. – 康桓瑋 Jul 28 '23 at 18:47
  • 3
    Using `span.extent` might be problematic though, as it might be `std::dynamic_extent`... – Jarod42 Jul 28 '23 at 19:29
4

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

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • 2
    Could return `std::array, N>` – Barry Jul 28 '23 at 20:11
  • I chose this as the answer because in the general case it is indeed important that the default constructor is not used. – phinz Jul 29 '23 at 06:55
  • Something I don't like is the fact that initialization is explicit per element such that for large `N` a lot of code is generated. I would prefer a method which internally exploits the linear arrangement of elements and that uses a loop internally, this solution seems to me like an unrolled loop. But it seems the standard library is lacking a better answer for my question. Do you think compilers can write the unrolled loop as normal loop again to avoid excessive code for large `N`? – phinz Jul 29 '23 at 06:59
  • @phinz you're right that the assembly output is huge, for example for https://godbolt.org/z/cYb9rfGqa. I've provided an alternative solution which fixes this for trivially copyable types, and which could cover more cases. – Jan Schultke Jul 29 '23 at 09:20
2

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.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • I do find it odd that you only check `is_trivially_copyable` before default-constructing – ildjarn Jul 30 '23 at 11:50
  • 1
    @ildjarn you're right, the type needs to be trivial, not just trivially copyable so that the optimization is possible. I've updated the answer. – Jan Schultke Jul 30 '23 at 12:20