Is there a concept in the standard library that tests for usability in ranged for loops
Asked Answered
C

1

8

There are a number of different ways, that make a type/class usable in a ranged for loop. An overview is for example given on cppreference:

range-expression is evaluated to determine the sequence or range to iterate. Each element of the sequence, in turn, is dereferenced and is used to initialize the variable with the type and name given in range-declaration.

begin_expr and end_expr are defined as follows:

  • If range-expression is an expression of array type, then begin_expr is __range and end_expr is (__range + __bound), where __bound is the number of elements in the array (if the array has unknown size or is of an incomplete type, the program is ill-formed)
  • If range-expression is an expression of a class type C that has both a member named begin and a member named end (regardless of the type or accessibility of such member), then begin_expr is __range.begin() and end_expr is __range.end()
  • Otherwise, begin_expr is begin(__range) and end_expr is end(__range), which are found via argument-dependent lookup (non-ADL lookup is not performed).

If I want to use a ranged for loop say in a function template, I want to constrain the type to be usable in a ranged for loop, to trigger a compiler error with a nice "constraint not satisfied" message. Consider the following example:

template<typename T>
requires requires (T arg) {
  // what to write here ??
}
void f(T arg) {
    for ( auto e : arg ) {
    }
}

Obviously for a generic function I want to support all of the above listed ways, that a type can use to make itself ranged for compatible.

This brings me to my questions:

  1. Is there a better way than manually combining all the different ways into a custom concept, is there some standard library concept that I can use for that? In the concepts library, there is no such thing. And if there is really no such thing, is there a reason for that?
  2. If there is no library/builtin concept for that, how am I supposed to implement such thing. What really puzzles me, is how to test for members begin and end regardless of the type or accessibility of such member (second bullet in the qouted list). A requires clause that tests for example for the existence of a begin() member fails if begin() is private, but the ranged for loop would be able to use the type regardless of that.

Note I am aware of the following two questions:

but neither of them really answers my question.

Chrisoula answered 14/4, 2022 at 8:14 Comment(3)
std::begin/std::end works for arrays, and will call the begin/end members if they exists. So that should cover all 3 cases already.Afterclap
I think you misunderstood the thing about private begin/ end. When the type has private begin and end then the range based for loop will attempt to use them and not fall back to std::begin and std::end. Hence the range based for loop is not able to use such type: godbolt.org/z/3dMPaPb57Bunko
@463035818_is_not_a_number that is actually a good point. Private begin/end members will cause the ranged for loop to not compile.Chrisoula
I
9

It seems like what you need is std::ranges::range which requires the expressions ranges::begin(t) and ranges::end(t) to be well-formed.

Where ranges::begin is defined in [range.access.begin]:

The name ranges​::​begin denotes a customization point object. Given a subexpression E with type T, let t be an lvalue that denotes the reified object for E. Then:

  • If E is an rvalue and enable_­borrowed_­range<remove_­cv_­t<T>> is false, ranges​::​begin(E) is ill-formed.

  • Otherwise, if T is an array type and remove_­all_­extents_­t<T> is an incomplete type, ranges​::​begin(E) is ill-formed with no diagnostic required.

  • Otherwise, if T is an array type, ranges​::​begin(E) is expression-equivalent to t + 0.

  • Otherwise, if auto(t.begin()) is a valid expression whose type models input_­or_­output_­iterator, ranges​::​begin(E) is expression-equivalent to auto(t.begin()).

  • Otherwise, if T is a class or enumeration type and auto(begin(t)) is a valid expression whose type models input_­or_­output_­iterator with overload resolution performed in a context in which unqualified lookup for begin finds only the declarations

    void begin(auto&) = delete; 
    void begin(const auto&) = delete;
    

    then ranges​::​begin(E) is expression-equivalent to auto(begin(t)) with overload resolution performed in the above context.

  • Otherwise, ranges​::​begin(E) is ill-formed.

That is to say, it will not only perform specific operations on the array type but also decide whether to invoke member function range.begin() or free function begin(range) based on the validity of the expression, this already covers the behavior described by the so-called range-expression. And ranges::end follows similar rules. So I think you can simply do

template<std::ranges::range T>
void f(T arg) {
  for (auto&& e : arg) { // guaranteed to work
  }
}

It should be noted that ranges::begin requires that the returned type must model input_or_output_iterator, and ranges::end also requires that the returned type must model sentinel_for type returned by ranges::begin, so that T is enough to be a range. The range-expression does not have such constraints, it only checks the validity of the expression, so a minimal type that can use a range-based for loop could be

struct I {
  int operator*();
  I& operator++();
  bool operator!=(const I&) const;
};

struct R {
  I begin();
  I end();
};

for (auto x : R{}) { } // well-formed

But I don't think you're interested in such a case since I is not sufficient to constitute a valid iterator.

Ives answered 14/4, 2022 at 8:23 Comment(1)
It even has a similar name :) Totally makes sense, that a "range" is required to be used in a "ranged for loop".Chrisoula

© 2022 - 2024 — McMap. All rights reserved.