is google::protobuf::RepeatedField<int>::iterator incrementable? compare to const int*/int*

88 views Asked by At

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 {};
1

There are 1 answers

2
Jan Schultke On

The difference is that std::begin doesn't return an lvalue reference, but a prvalue. Let's examine the two types closer:

decltype(++std::declval<int*&>()) pri = { nullptr };

Here, std::declval gives us an int*&, which is converted to an lvalue of type int* in the expression. This allows us to use the pre-increment operator, which preserves the value category.

Furthermore, decltype results in int*&, and this lvalue reference cannot bind to a prvalue of type nullptr. That declaration is ill-formed, but the type is well-formed.


decltype(++(std::begin(std::declval<google::protobuf::RepeatedField<int>&>()))) nr{nullptr};

In this example, std::begin() would return a prvalue of type RepeatedField<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::RepatedField has an iterator which is defined as:

typedef Element* iterator;

... 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_traits exists 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 alias std::iterator_traits<Iter>::iterator_category.

#include <type_traits>
#include <algorithm>
#include <array>

// convenience alias to get the iterator type of a range
template <typename T>
using iterator_t = decltype(std::begin(std::declval<T&>()));

// primary template for types that don't have any iterator traits
// use void_t idiom for member detection
template <typename Range, typename Traits = void>
struct is_forward_range_impl : std::false_type {};

// substitution will succeed only if we can use std::begin and std::end,
// and if the range's iterator has std::iterator_traits
template <typename Range>
struct is_forward_range_impl<
    Range,
    std::void_t<decltype(std::begin(std::declval<Range&>())),
                decltype(std::end(std::declval<Range&>())),
                std::iterator_traits<iterator_t<Range>>>>
{
private:
    using traits = std::iterator_traits<iterator_t<Range>>;
    using category = typename traits::iterator_category;
    using value_type = typename traits::value_type;

public:
    static inline constexpr bool value
        = std::is_convertible_v<category, std::forward_iterator_tag>;
};

All that is left is giving this implementation a proper interface:

// proper type trait with no extraneous template parameters
template <typename Range>
struct is_forward_range : is_forward_range_impl<Range> {};

// convenience variable template
template <typename Range>
inline constexpr bool is_forward_range_v = is_forward_range<Range>::value;

We can use this trait as follows:

// should pass
static_assert(is_forward_range_v<int[10]>);
static_assert(is_forward_range_v<std::array<int, 10>>);

// should pass too
struct S {};
static_assert(!is_forward_range_v<S>);

See live example on Compiler Explorer