Home > database >  Problems with template argument deduction when extending std::span
Problems with template argument deduction when extending std::span

Time:10-24

I am trying to extend std::span to have a bounds checked operator[] (I am aware gsl::span provides this)

I have declared my container as follows:

#include <span>
#include <string>
#include <utility>
#include <stdexcept>

template <typename ... TopArgs> class BoundsSpan : private std::span<TopArgs...> {
public:
    typename std::span<TopArgs...>::reference operator[](std::size_t idx) const {
        if (idx >= this->size()) [[unlikely]] {
            throw std::out_of_range(std::string("span out of bounds access detected - wanted index ["   std::to_string(idx)   "] but size is "  
                                                std::to_string(this->size())));
        }
        return std::span<TopArgs...>::operator[](idx);
    }
    template<typename ... Args>
    BoundsSpan(Args&& ... args) : std::span<TopArgs...>(std::forward<Args>(args) ...) {}
};

This seems to work great, however I noticed that template argument deduction no longer works. E.g.

int main(int argc, char *argv[]) {
    BoundsSpan span(argv, argc);
}

gives a "Too few template arguments for class template 'span'" and requires an explicit BoundsSpan<char*> instead - this is not the case with regular std::span

Additionally, constructing a BoundsSpan on a C-style array with runtime length gives a "variably modified type cannot be used as a template argument" - did I overlook a template specialization here? An example reproducer would be

void func(int len) {
    int arr[len];
    BoundsSpan<int> span(arr, len);
}

CodePudding user response:

The std::span template parameter list is not compatible with your variadic parameter list. It takes:

template <typename T, std::size_t N>
class span {...};

However, your class is working in terms of a variadic type list. There is no way for you to propagate the template parameters "up" to the span base when it wants a non-type template parameter, and you only pass types. It's best to try matching the template signature of the span you're impersonating. That brings some simplifications to the problems:

  • you can expose all of the span constructors as your own (i.e. using std::span<T, N>::span;)

  • You can use the same deduction guides that the std::span has, renamed for your class.

Here's a revised version of your code that I found to be functional:

template <class T, std::size_t N = std::dynamic_extent> 
class BoundsSpan : private std::span<T, N> {
public:
    using std::span<T, N>::span;
    using std::span<T, N>::data;
    using std::span<T, N>::size;
    // .. and all the other interface functions

    typename std::span<T, N>::reference operator[](std::size_t idx) const {
        if (idx >= this->size()) [[unlikely]] {
            throw std::out_of_range("span out of bounds - wanted index ["   
                std::to_string(idx)   "] but size is "   std::to_string(size()));
        }
        return std::span<T, N>::operator[](idx);
    }
};

Then taking std::span's deduction guides for inspiration yields:

template <class It, class EndOrSize>
BoundsSpan(It, EndOrSize) -> BoundsSpan<std::remove_reference_t<std::iter_reference_t<It>>>;
        
template<class T, std::size_t N>
BoundsSpan(T (&)[N]) -> BoundsSpan<T, N>;
        
template<class T, std::size_t N>
BoundsSpan(std::array<T, N>&) -> BoundsSpan<T, N>;
        
template<class T, std::size_t N>
BoundsSpan(const std::array<T, N>&) -> BoundsSpan<const T, N>;
        
template<class R>
BoundsSpan(R&&) -> 
    BoundsSpan<std::remove_reference_t<std::ranges::range_reference_t<R>>>;

With this it should work in your examples and like span. You might want to also wrap the const version of operator[].

https://godbolt.org/z/7ec3PYe7e

  • Related