I would like to do roughly this:
foo( Container<std::string> container)
that accepts any iterable container as long as it contains stringsfoo
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)
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:
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.
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
}
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
.
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
).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With