Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the intended way of passing c++ Views and Ranges to functions?

Tags:

c++

Usage Scenario

I would like to do roughly this:

  • Write a function foo( Container<std::string> container) that accepts any iterable container as long as it contains strings
  • Call foo and give it the values I get from filtering a std::vector<string> like this:
   std::vector<std::string> myList = { "a", "b" };
   auto filteredList = myList | std::ranges::views::filter([](std::string elem) {
       return elem == "a";
   });
   foo(filteredList)

Attempt

I come from other languages, where there is the concept of iterable interfaces. The Q&A "What is the correct approach to passing a std::ranges::view type object to a class?" explains parts for this:

  • how we can create a templated function that can accept a view as a parameter
  • how to use requires to ensure the element type must be std::string

We get something like this:

template<std::ranges::view ViewOfDetections> requires std::same_as<std::ranges::range_value_t<ViewOfDetections>, std::string>
void foo (ViewOfDetections container){
  // implementation must be here.
}

However, this seems very clunky:

  • The use of templates means that we can not separate the declaration from the code. All of it must be together in the header file (unless I misunderstood this. But it does not seem to work to split it up because what comes after the template part is not a class template.)

  • The declaration type is not very readable. Other languages have some interface type that you can specify instead.

Question

It very much feels like I fundamentally misunderstood something or missed a basic std feature.

What is the correct, established, elegant, maintainable, readable way to do write a function that can operate on any iterable container - i.e. on a view or a range - of a specific element type?


Another way of doing this is outlined in "Declare template function to accept any container but only one contained type" but it comes with the same drawbacks and even worse readability:

    // template: a Container that takes variadic arguments, and the said arguments (types).
    // Whatever we pass in will be fed our required type std::string as first type template argument.
    // The other arguments are figured out by the compiler, if they are required.
    // This declaration in itself would also accept anything else that is templated, not just containers.
    // But that would then lead to a derived template resolution failure.
    template< template < class ... > typename Container, class ... Args>
    void foo(Container<std::string, Args...> container)
    {
        // do something with the container elements
    }

Related Links

For the benefit of future readers, this Q&A "Range Concept for a specific type" might also be of interest. It discusses same_as vs convertible_to and range_value_t vs range_reference_t.

like image 778
lucidbrot Avatar asked Oct 16 '25 00:10

lucidbrot


2 Answers

Firstly, if you use a template, the concept should be std::ranges::input_range (or some other ??_range, not view). This allows you to directly accept containers.

Second - yes, this is clunky, and only makes sense for generic algorithms (like those in <algorithm>). Higher-level code should probably accept std::span<const T>, and it's the job of the caller to convert their views into a contiguous container.

You could come up with a type-erased class (think std::function for ranges) that lets you accept any range without templating the function, but this would have performance overhead (compared to, say, span).

like image 177
HolyBlackCat Avatar answered Oct 17 '25 16:10

HolyBlackCat


My personal way would be two templates:

template <typename Iterator>
void foo(Iterator begin, Iterator end)
    requires(std::is_same_v<std::remove_cvref_t<decltype(*begin)>, std::string>)
{
    static_assert(std::is_same_v<std::remove_cvref_t<decltype(*begin)>, std::string>);
    // ...
}

template <typename Container>
void foo(Container /*const*/& c)
//                   ^ const, if foo is not intended to modify the container!
{
    foo(std::begin(c), std::end(c));
}

The static_assert might be an alternative if you want to keep the function declaration more compact, I'd rather prefer the requires clause instead, though, as it shows right at the declaration what actually is expected.

like image 39
Aconcagua Avatar answered Oct 17 '25 15:10

Aconcagua