C++ – How to Write a C++20 Function Accepting a Range with Both T and Const T

c++c++20genericsstd-rangestemplates

I defined the following function:

template<
    std::ranges::contiguous_range R,
    typename T = std::ranges::range_value_t<R>
>
std::span<T> foo(R&& r, const T& someValue) {
    std::span<T> sp{r.begin(), r.end()};
    /// ...
    return sp;
}

Now I have three use cases:

std::vector<std::string> baseVec;
auto a = foo(baseVec, {""});

std::span<std::string> sp{baseVec};
auto b = foo(sp, {""});

const std::vector<std::string>& ref = baseVec;
auto c = foo(ref, {""}); // <------------------ problem here!

As far as I could understand, foo(ref) will fail to compile because the span created inside foo is of type span<T>, whereas in this case it should be span<const T>.

So, how can I write foo so it accepts all the three cases?

Best Answer

The issue is that the value_type of a range is never const-qualified. But reference type of the range could be - although there that's still wrong because it'd be string const&.

Contiguous iterators are required to have their reference type to be U& for some type U, so if we simply drop the trailing reference we'll get a potentially const-qualified type:

template<
    std::ranges::contiguous_range R,
    typename T = std::remove_reference_t<std::ranges::range_reference_t<R>>
>
std::span<T> foo(R&& r, const T& someValue) {
    std::span<T> sp(r);
    /// ...
    return sp;
}

Note that you don't need to pass r.begin() and r.end() into span, span has a range constructor.

However, this still isn't quite correct for a reason that the original was also wrong. contiguous is an insufficient criteria here, we also need the range to be sized so that we can construct a span out of it:

template<
    std::ranges::contiguous_range R,
    typename T = std::remove_reference_t<std::ranges::range_reference_t<R>>
>
    requires std::ranges::sized_range<R>
std::span<T> foo(R&& r, const T& someValue) {
    std::span<T> sp(r);
    /// ...
    return sp;
}

Also you probably don't want to deduce T from the argument - you really want it to be specifically the correct T associated with the range:

template<
    std::ranges::contiguous_range R,
    typename T = std::remove_reference_t<std::ranges::range_reference_t<R>>
>
    requires std::ranges::sized_range<R>
std::span<T> foo(R&& r, std::type_identity_t<T> const& someValue) {
    std::span<T> sp(r);
    /// ...
    return sp;
}

Note that I changed your code to construct the span using parentheses instead of braces. This is because passing an iterator pair into an initializer using braces is not a good idea, because it could easily do the wrong thing. Consider:

auto x = std::vector{1, 2, 3, 4};

auto a = std::vector(x.begin(), x.end()); // four ints: [1, 2, 3, 4]
auto b = std::vector{x.begin(), x.end()}; // two iterators: [x.begin(), x.end()]

And the same is true for span, which will have an initializer_list constructor starting in C++26.