6

Say I have some template function that returns the median of some iterable object passed into it.

Something like:

template<typename T>
decltype(auto) find_median_sorted(T begin)
{
  // some code here
}

Now I want to make sure I constrained T to always be iterable. I am trying to learn how to use concepts in C++, so is there some way I can use concept here to make sure T is iterable?

I'm sure there are other ways to check if it is iterable, but is this a wrong use case for concepts? I also came across this post here related to element iterable, but I am not sure how this applies to my case.

JeJo
  • 30,635
  • 6
  • 49
  • 88
beep_boop
  • 819
  • 6
  • 15
  • There are several different [iterator concepts](https://en.cppreference.com/w/cpp/iterator), and you could probably pick the broadest one that still allows you to write the algorithm you have in mind. Perhaps `random_access_iterator`? – Nathan Pierson Nov 02 '21 at 05:03
  • @NathanPierson yes thank you for that, I can use some iterator concept. However, I am a bit confused as to how my syntax would like like if I was to use say `random_acces_iterator`. What would the function signature look like? – beep_boop Nov 02 '21 at 05:05
  • The constraints of a function are generally closely related to its implementation. What is an iterable object you are referring to? Is it an iterator? Or is it a range that can be iterated with iterator? In addition, the type of `len` should not be `int`, which is not generic. – 康桓瑋 Nov 02 '21 at 05:28
  • FYI the `declspec(auto)` seems suspect here. I would expect a median-calculating function to return a regular `auto`, since I don't see when you would ever want to return a reference here. –  Nov 02 '21 at 05:37

2 Answers2

8

I am trying to learn how to use concepts in C++, so is there some way I can use concept here to make sure T is iterable?

You can have standard concept std::ranges::range from <ranges> header here. With that your function will look like:

#include <ranges>  // std::ranges::range

template<std::ranges::range T>
decltype(auto) find_median_sorted(T const& container) {
    // some code here
}

To restrict the ranges to be only for random access iterable ranges, you can use either std::ranges::random_access_range again from <ranges> header

#include <ranges>  // std::ranges::random_access_range

template<std::ranges::random_access_range T>
decltype(auto) find_median_sorted(T const& container) {
    // some code here
}

or via iterator concept std::random_access_iterator as follows:

#include <iterator> // std::random_access_iterator 

template<typename  T>
decltype(auto) find_median_sorted(T const& container)
requires std::random_access_iterator<std::ranges::iterator_t<T>>
{
    // some code here
}
Jarod42
  • 203,559
  • 14
  • 181
  • 302
JeJo
  • 30,635
  • 6
  • 49
  • 88
  • Passing a range and a length seems weird to me. Why bother when making a subrange/view at the callsite is trivial? –  Nov 02 '21 at 05:28
  • First, your function pass by value, which means that it will copy the entire vector into your function. In addition, you should use `random_access_range` to constrain `T` instead of `random_access_iterator`. – 康桓瑋 Nov 02 '21 at 05:32
  • @Frank thanks for bringing that up. You're right, it is redundant if I use ranges, will remove it. – beep_boop Nov 02 '21 at 05:32
  • 2
    `decltype(conatiner.begin())` cannot be applied to the raw array. You should use `iterator_t`. – 康桓瑋 Nov 02 '21 at 05:48
3

Edit: The original code in the question looked a lot more like iterators, hence why an iterator-based answer. As it stands, the range-based answer is a better fit for OP's request.

An important distinction between "old-school" iterators and their modern interpretation is that the sentinel (aka end iterator) does not necessarily have to be the same type as the begin one.

So you should be using 2 separate constraints. One for the iterator itself, and another one for the sentinel, using std::sentinel_for:

As for the iterator itself, you will want to use the loosest iterator concept that allows you to perform the work you want to do. Ideally std::input_iterator if possible.

Applying the constraint is simply a matter of replacing typename in the template by the appropriate concept:

#include <iterator>

template<std::input_iterator I, std::sentinel_for<I> S>
auto find_median_sorted(I begin, S end){
  // some code here
}
  • Thank you for your answer! But how is this better than using std::ranges::range in the template? Does this make the code more "generic" and makes it adapt to broader range of objects? – beep_boop Nov 02 '21 at 05:30
  • 1
    @beep_boop It's functionally equivalent. Depending on what goes on in the function and how the function is used, one or the other might lead to cleaner code. Your question was about how to do constrained iterators, so I felt like an answer that does that literally was appropriate. –  Nov 02 '21 at 05:34
  • 1
    Specifically, you can always get a pair of iterators from a range, and you can make a range from an iterator and its sentinel, so either will work. –  Nov 02 '21 at 05:35
  • Only constraint `std::input_iterator` is too loose here. – 康桓瑋 Nov 02 '21 at 05:53
  • Strictest? Almost all iterators are `input_iterator`. – 康桓瑋 Nov 02 '21 at 05:58
  • @康桓瑋 You are correct, it's my wording that was wrong, I meant "loosest", I've ammended the answer and am heading to bed... –  Nov 02 '21 at 06:00
  • @康桓瑋 there is an NSFW joke somewhere in there... – Swift - Friday Pie Nov 02 '21 at 06:37