Search code examples
c++stlgsl

How to efficiently copy from gsl::span to std::vector?


To copy from a C++20 std::span into an std::vector I can simply do the following:

void WriteBuffer(std::vector<std::byte>& destData, const std::span<const std::byte>& srcData)
{
    std::copy(srcData.begin(), srcData.end(), destData.end());
}

..which will be optimised to a single memmove. See example: https://godbolt.org/z/8nPjzj3c6

However, if I replace std::span with gsl::span we don't get this optimisation anymore: https://godbolt.org/z/MWfPKW8eq

Is it best to instead resize the vector and use std::memcpy to copy over the data, or are there better alternatives?

My understanding is that gsl::span does not get optimised, because it's using a custom iterator that the implementation of std::copy is not aware of - so it cannot make assumptions about the linearity of the data - is that right? If so, I suppose all STL containers and algorithms will have the same issue with gsl::span and any other data structures that use custom iterators. But please let me know if I've misunderstood something.

Update: Sorry, there was a mistake here. As @Caleth kindly pointed out, I'm writing out of bounds. However, resizing first and then using copying to destData.begin() (yes overwriting, just for simplicity..) doesn't really change it - it still copies byte-by-byte.


Solution

  • because it's using a custom iterator that the implementation of std::copy is not aware of

    Not really. The standard library would see this member and have the same knowledge about the data layout.

    #if defined(__cpp_lib_ranges) || (defined(_MSVC_STL_VERSION) && defined(__cpp_lib_concepts))
        using iterator_concept = std::contiguous_iterator_tag;
    #endif // __cpp_lib_ranges
    

    The more likely reason is that the iterator operations are checked at runtime, rather than the "undefined behaviour when misused" of std::span.

    If you were to read out of range of srcData, using std::span the behaviour is undefined. Using gsl::span, the behaviour is defined, std::terminate is called. The "missing optimisations" are probably because memcpying out of range data is not the same as calling std::terminate

    Your example code exhibits undefined behaviour when srcData has any elements, because you can't write past the end of a vector.