What's the difference in type deduction? The second line fails, but why?
decltype(++std::declval<int*&>()) pri = {nullptr};
decltype(++(std::begin(std::declval<google::protobuf::RepeatedField<int>&>()))) nr{nullptr};
error: lvalue required as increment operand
Despite the fact that the following assertion passes:
using iterator = decltype((std::begin(std::declval<google::protobuf::RepeatedField<int>&>())));
static_assert(std::is_same_v<iterator, int*>);
A RepeatedField<int> definitely has a int* as an iterator, which is incrementable, so how come the result of std::begin cannot be incremented?
Generally, my problem is with the detection of whether a type is a "range".
The code below works and recognizes RepeatedField as range.
However, when using SFINAE with functions, it doesn't work. (is_range_helper<T>(0) which too could be fixed by std::next)
template <typename T, typename = void>
struct is_range: std::false_type {};
template <typename T>
struct is_range<T, std::void_t<decltype(
++std::begin(std::declval<T&>()),
std::begin(std::declval<T&>()),
std::end(std::declval<T&>()),
++std::begin(std::declval<T&>()), // problem here
*std::begin(std::declval<T&>())
)>> : std::true_type {};
The difference is that
std::begindoesn't return an lvalue reference, but a prvalue. Let's examine the two types closer:Here,
std::declvalgives us anint*&, which is converted to an lvalue of typeint*in the expression. This allows us to use the pre-increment operator, which preserves the value category.Furthermore,
decltyperesults inint*&, and this lvalue reference cannot bind to a prvalue of typenullptr. That declaration is ill-formed, but the type is well-formed.In this example,
std::begin()would return a prvalue of typeRepeatedField<int>::iterator, and the pre-increment operator cannot be applied to it. The type is ill-formed, and the declaration as a whole is ill-formed.On a side note, iterators might support pre-increment of prvalues, if the increment is an operator overload, not a builtin operator. However,
google::protobuf::RepatedFieldhas aniteratorwhich is defined as:... so all the regular restrictions apply.
Solution
Generally, there are a lot more requirements for iterators than just being incrementable. We could create the traits to detect all the different requirements for e.g. a ForwardIterator, but it would be a lot of effort.
Thankfully, for any iterators satisfying the requirements of iterators,
std::iterator_traitsexists and lets us access all information about them, such as the iterator category. For example, we can detect that something is a ForwardIterator by checking the type of the aliasstd::iterator_traits<Iter>::iterator_category.All that is left is giving this implementation a proper interface:
We can use this trait as follows:
See live example on Compiler Explorer