user3188445
user3188445

Reputation: 4784

Best practice for declaring functions that won't modify a std::span in C++

I have a bunch of template functions that do things on spans. Some of these functions modify what the span points to, while others never change the span. I would like to make the distinction between these two types of function evident in the type of the function and guard against accidentally changing a span in what should be a read-only function. My question: what is the best way to do this?

An example of what does not work:

#include <span>
#include <string>

using namespace std;

template<typename B> void want_span(std::span<B>) {};

template<typename B> void want_cspan(std::span<const B>) {};

int
main()
{
  std::string r;
  want_span(std::span(r));  // OK
  want_cspan(std::span(r)); // Error: Can't infer type of B
}

I can of course explicitly cast r to a std::span<const char>, but this is quite clumsy. I could also introduce a constant span type, cspan, but this also requires an explicit cast to cspan, and doesn't work with a span:

template<typename B> struct cspan : std::span<const B> {
  using std::span<const B>::span;
};
template<std::ranges::contiguous_range R>
cspan(R &&) -> cspan<std::remove_const_t<std::ranges::range_value_t<R>>>;

template<typename B> void want_cspan2(cspan<B>) {};

int
main()
{
  std::string r;
  want_cspan2(cspan(r)); // OK
  want_cspan2(std::span(r)); // Error
}

Is there really no convenient way to make a function that takes a std::span but promises not to modify the objects referenced by the span? Ideally I could take a function that used to be non-const and make it const, and have all existing invocations continue working. This must be a reasonably common pattern, so I'm wondering what the best practice is here.

Upvotes: 3

Views: 206

Answers (2)

Eugene
Eugene

Reputation: 7688

std::span<T> is intended to be used with a concrete type T, not with template<typename T>. std::span<T> is essentially a type-erased std::ranges::contiguous_range concept. The advantage of using it as a function parameter is that it avoids templates and still accepts arguments like std::vector<T> and std::array<T>. If you are going to use a template anyway, it is better to use a range as a template argument:

#include <ranges>
#include <string>

namespace rng = std::ranges;

void want_span(rng::contiguous_range auto& r) {};
void want_cspan(rng::contiguous_range auto const& r) {};

int main()
{
  std::string r;
  want_span(r);
  want_cspan(r); 
}

Not only do you get rid of the problem with accepting ranges of non-const arguments in want_cspan(r);, but you also do not need to use std::span on the call site, i.e., you write want_span(r); rather than want_span(std::span(r));

Upvotes: -1

Jarod42
Jarod42

Reputation: 218138

I have a bunch of template functions

It is the "issue" with template, their template parameter should be deduced or provided.

If you don't want to change call site, you might add extra overload to do the conversion for you; for example:

template<typename T> void want_cspan(std::span<const T>) { /* .. */ }
template<typename R> void want_cspan(const R& r) {
    want_cspan<std::remove_reference_t<std::ranges::range_reference_t<R>>>(r);
}

Demo

Upvotes: 3

Related Questions