8
\$\begingroup\$

I really like gsl::span and use it occasionally. However, sometimes you don't know what type of data, if at all, your span of memory is. And, in fact, a lot of the functionality of gsl::span is not type-specific. So I stripped it of the type-specific code, made a few tweaks, and voilà - my memory_region class (sans include guards):

#if ( __cplusplus < 201103L )
#error "C++11 support required for the memory_region class"
#endif

#include "gsl/gsl-lite.h"
#include <cstring> // for memcmp

#ifdef __CUDACC__
#define CUDA_DESIGNATOR __host__ __device__
#ifdef Expects
#undef Expects
#undef Ensures
#define Expects(x)
#define Ensures(x)
#endif
#else /* non-CUDA code */
#define CUDA_DESIGNATOR
#ifndef Expects
#define Expects(x)  ::gsl::fail_fast_assert((x))
#define Ensures(x)  ::gsl::fail_fast_assert((x))
#endif
#endif

namespace util {

class memory_region
{
public:
    typedef size_t       size_type;
    typedef void *       pointer;
    typedef void const * const_pointer;

    pointer   data_  { nullptr };
    size_type size_  { 0 }; // in bytes

    CUDA_DESIGNATOR memory_region() { }

    CUDA_DESIGNATOR memory_region( pointer data, size_type size )
        : data_ ( data )
        , size_ ( size )
    {
        Expects( size == 0 || ( size > 0 && data != nullptr ) );
    }

    template < class U >
    CUDA_DESIGNATOR memory_region( U* begin, U* end )
        : data_ ( begin )
        , size_ ( reinterpret_cast<char*>(end) - reinterpret_cast<char*>(begin) )
    {
        Expects( begin <= end );
    }

    // In gsl::span, this is private
    template< typename U >
    CUDA_DESIGNATOR memory_region( U * & data, size_type size )
        : data_ ( data )
        , size_ ( size )
    {
        Expects( size == 0 || ( size > 0 && data != nullptr ) );
    }

    // In gsl::span, this is private
    template< typename U >
    CUDA_DESIGNATOR memory_region( U * const & data, size_type size )
        : data_ ( data )
        , size_ ( size )
    {
        Expects( size == 0 || ( size > 0 && data != nullptr ) );
    }


    template< class U, size_t N >
    CUDA_DESIGNATOR memory_region( U (&arr)[N] )
        : data_ ( arr )
        , size_ ( N )
    {}

    template< class U, size_t N >
    CUDA_DESIGNATOR memory_region( std::array< U, N > & arr )
        : data_ ( arr.data() )
        , size_ ( N )
    {}

#if gsl_HAVE_DEFAULT_FUNCTION_TEMPLATE_ARG && gsl_HAVE_CONTAINER_DATA_METHOD
    // SFINAE enable only if Cont has a data() member function
    template< class Cont, typename = decltype(std::declval<Cont>().data()) >
    CUDA_DESIGNATOR memory_region( Cont & cont )
        : data_ ( cont.data() )
        , size_ ( cont.size() )
#else
    template< class Cont >
    CUDA_DESIGNATOR memory_region( Cont & cont )
        : ptr_ ( cont.size() == 0 ? nullptr : &cont[0] )
        , end_ ( cont.size() )
#endif
    {}

    CUDA_DESIGNATOR memory_region( memory_region && ) = default;
    CUDA_DESIGNATOR memory_region( memory_region const & ) = default;

    template< typename U >
    CUDA_DESIGNATOR memory_region( gsl::span<U> const & s )
        : data_ ( s.begin() )
        , size_ ( s.size() )
    {}

    CUDA_DESIGNATOR memory_region & operator=( memory_region && ) = default;
    CUDA_DESIGNATOR memory_region & operator=( memory_region const & ) = default;

    // TODO: construct from gsl::span

    CUDA_DESIGNATOR memory_region subbuffer( size_type offset ) const noexcept
    {
        Expects( offset >= 0 && offset < this->size() );
        return memory_region( reinterpret_cast<char*>(data_) + offset,
            this->length() - offset );
    }

    CUDA_DESIGNATOR memory_region subbuffer( size_type offset, size_type count ) const noexcept
    {
        Expects( offset >= 0 && offset < this->size() && count <= this->size() - offset );
        return memory_region( reinterpret_cast<char*>(data_) + offset, count );
    }

    CUDA_DESIGNATOR operator bool () const noexcept
    {
        return data_ != nullptr;
    }

    // Note: Behavior here is _unlike_ span - we don't compare bytes
    CUDA_DESIGNATOR bool operator==( memory_region const & other ) const noexcept
    {
        return  size() == other.size() && (data_ == other.data_ );
    }

    CUDA_DESIGNATOR bool operator!=( memory_region const & other ) const noexcept
    {
        return !( *this == other );
    }

    CUDA_DESIGNATOR pointer data() const noexcept
    {
        return data_;
    }

    CUDA_DESIGNATOR bool empty() const noexcept
    {
        return size() == 0;
    }

    CUDA_DESIGNATOR size_type size() const noexcept
    {
        return size_;
    }

    CUDA_DESIGNATOR size_type length() const noexcept
    {
        return size();
    }

    CUDA_DESIGNATOR size_type bytes() const noexcept
    {
        return size();
    }

    void swap( memory_region & other ) noexcept
    {
        using std::swap;
        swap( data_, other.data_ );
        swap( size_, other.size_ );
    }

    CUDA_DESIGNATOR gsl::span< const gsl::byte > as_bytes() const noexcept
    {
        return gsl::span< const gsl::byte >( reinterpret_cast<const gsl::byte *>( data() ), bytes() );
    }

    CUDA_DESIGNATOR gsl::span< gsl::byte > as_writeable_bytes() const noexcept
    {
        return gsl::span< gsl::byte >( reinterpret_cast<gsl::byte *>( data() ), bytes() );
    }

    template< typename U >
    gsl::span< U > as_span() const noexcept
    {
        Expects( ( this->bytes() % sizeof(U) ) == 0 );
        return gsl::span< U >( reinterpret_cast<U *>( this->data() ), this->bytes() / sizeof( U ) );
    }

    // The most un-span-like behavior of memory_region: decaying to the pointer
    operator void*() const noexcept { return data(); }
};

// memory_region creator functions (see ctors)

template< typename T >
CUDA_DESIGNATOR memory_region as_memory_region( T * begin, T * end )
{
    return memory_region( begin, end );
}

template< typename T >
CUDA_DESIGNATOR memory_region as_memory_region( T * begin, size_t size )
{
    return memory_region( begin, size );
}

template< typename T, size_t N >
CUDA_DESIGNATOR memory_region as_memory_region( T (&arr)[N] )
{
    return memory_region( arr, N );
}

template< typename T, size_t N >
CUDA_DESIGNATOR memory_region as_memory_region( std::array<T,N> & arr )
{
    return memory_region( arr );
}

template< class Cont >
CUDA_DESIGNATOR auto as_memory_region( Cont & cont ) ->  memory_region
{
    return memory_region( cont );
}

// ... and a span creator

template< typename U >
CUDA_DESIGNATOR const gsl::span< U > as_span(const  memory_region& region )
{
    return region.as_span<U>();
}

template< typename U >
CUDA_DESIGNATOR gsl::span< U > as_span( memory_region& region )
{
    return region.as_span<U>();
}


} // namespace util

#endif /* SRC_UTIL_MEMORY_REGION_H_ */

Questions:

  • Should I use C++17's std::byte or gsl::byte and have byte *'s instead of void *s?
  • How legitimate is it that I also have memory_region decay into a void *?
  • I'm at a loss about what to do with this class when you want the memory itself (as opposed to the span structure) to be const. With gsl::span you can at least say gsl::span<const int> and use the templating, but my class is not templated. Should I template it on a boolean indication of constness? That doesn't feel right.
  • Is this class a good idea in your opinion - at all, and specifically as a potential base class for gsl::span?
\$\endgroup\$
  • \$\begingroup\$ If you want an untyped version of span, I would make it a specialization of span for void. That most closely resembles void* p+size_t size. \$\endgroup\$ – Deduplicator Jul 30 '17 at 20:11
  • \$\begingroup\$ @Deduplicator: gsl::span is not mine to specialize. In fact, it'll probably go into the standard library in C++20. On the contrary, I was actually thinking of maybe suggesting my idea to some GSL implementors as a base class. Also, this class does is missing quite a bit of the functionality of gsl::span. \$\endgroup\$ – einpoklum Jul 30 '17 at 22:12
  • \$\begingroup\$ Well, all that is true. Still, ideally it should end up as the span<cv-qualified void>-specializations... \$\endgroup\$ – Deduplicator Jul 30 '17 at 22:17
  • \$\begingroup\$ @Deduplicator: A specialization which cuts out a bunch of functionality? I dunno :-( \$\endgroup\$ – einpoklum Jul 30 '17 at 22:18
  • \$\begingroup\$ Certainly. Look how smart-pointers (which are a surprisingly close analogue) handle void. \$\endgroup\$ – Deduplicator Jul 30 '17 at 22:22
2
\$\begingroup\$

Implementation

  • Wrong sizes: many constructors set size_ to a number of U elements, not a number of bytes!
  • The constructor with template argument Cont in the #else branch tries to initialize ptr_ and end_ members - which don't exist (data_ and size_ were probably meant).
  • Comment //TODO: construct from gsl::span 5 lines below said constructor is confusing.
  • The top part of the include guard was omitted, but the bottom part is still there.
  • const_pointer isn't actually used anywhere.
  • Ensures isn't actually used anywhere.
  • Contents of <cstring> aren't actually used anywhere (not even memcmp that the comment indicates).
  • as_memory_region(Cont& cont) is declared inconsistently, without apparent reason.

Answers

  • I'd say it depends on the expected usage scenario: Do you just pass around regions of memory without caring what's inside them (e.g. inside allocators), or do you want to read the bytes in the region? In the first case, I'd stick to void *, in the second std::byte * (or maybe gsl::byte *) would be more fitting. But in that case, why not use gsl::span<std::byte> or similar?
  • I personally don't like the decay into void *: Basically the whole idea behind the structure is to bind a size to a pointer into memory. I can't think of a case where decaying into a pointer without size information would actually be useful and necessary in a way that can't be done by calling data(). Even weirder, now you can easily enter undefined behavior by trying to compare two memory_region objects: m1 < m2 isn't directly supported by the implementation, but the casts to void allow that comparison to be made - in a way that isn't defined in all cases.
  • I don't think that point is an issue: You can't create void objects, so you can't write them! In any attempt to modify the memory, the first thing to do would be casting data() to a pointer of the appropriate type - and a sufficiently determined user would just remove the constness with that cast anyways.
  • In it's current state, it might be a good idea for cases where regions of memory, regardless of their content, need to be managed (e.g. an allocator). I don't think it would make a great base class for gsl::span: They have subtle differences in meaning. memory_region allows passing a region of indeterminable memory (and size), not caring about the contents of the memory. gsl::span<T> is about passing a contiguous array of elements, whose values are important. While it could make a worthwhile specialization of gsl::span<void> (or maybe gsl::span<const void>), I think those differences are enough to warrant a different class with different usage semantics.
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thank you. Assuming I fix the minor issues you mentioned above, and removed the capability of holding indeterminate-length regions - would your answer change? Specifically, like you said, gsl::span<T> is about a contiguous array of elements, but if that contiguous array fills up a contiguous region of memory with a certain size, which is what my class is about. I'm not sure how I feel about a specialization, though. \$\endgroup\$ – einpoklum Oct 31 '17 at 15:31
  • \$\begingroup\$ @einpoklum: I don't think so. Dealing in chunks of memory is (on a fundamental level) conceptually different from dealing in chunks of elements, so I don't feel those different concepts should be shoehorned into one class or inheritance structure. Yes, they seem similar on a basic level (pointer + size), but the intended semantics and usage scenarios differ. \$\endgroup\$ – hoffmale Oct 31 '17 at 16:27
  • \$\begingroup\$ ... even if the functionality becomes a strict subset of the functionality of a span? \$\endgroup\$ – einpoklum Oct 31 '17 at 17:29
  • \$\begingroup\$ @einpoklum: AFAICT, the core functionality of gsl::span is operator[], allowing random access into the underlying elements - which cannot be provided in any meaningful manner for memory_region. \$\endgroup\$ – hoffmale Oct 31 '17 at 17:37
  • \$\begingroup\$ Exactly... that's why I was thinking it could be a base class, not a specialization or a subclass: Does everything a memory region does, and then some. \$\endgroup\$ – einpoklum Oct 31 '17 at 17:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.