0

I'm struggling to satisfy a C++20 concept for my class.

struct C1
{
    int* begin();
    int* end();
};

static_assert(std::ranges::range<C1>);  // Compiles fine

struct C2 {};

namespace std::ranges    // (A)
{
    auto begin(C2&);
    auto end(C2&);
}

static_assert(std::ranges::range<C2>);  // Fails to compile

The std::ranges::range<> concept is defined as follows:

template <class _Rng>
concept range = requires(_Rng& __r) {
  std::ranges::begin(__r);
  std::ranges::end(__r);
};

Taking this literally instructs me to declare the functions (A) above. But these doen't help. On the other hand, defining the two member functions as in class C1 does work. My problem is that I just don't see the semantic connection between the requirement imposed by the concept and the solution that helps for class C1. It tells me to do one thing when in fact I need to do another thing.

Please provide some explanation for this.

Background: In my production code, my class does define a begin() and end() function that return an iterator each. But the range concept is not satisfied with these definitions and fails to show me why.

See the full example on Compiler Explorer

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
Frank Heimes
  • 100
  • 6

3 Answers3

8

std::ranges::begin is a customization point object; it's not a function you can overload. And even if begin were a proper function, you cannot insert overloads of functions into the std namespace or any sub-namespace (you can specialize templates in std, but only if it involves at least one user-defined type).

The way to conform to std::ranges::begin is by following its rules. After checking that the given range type is an array (for which it has specialized logic), it will look for, in order:

  • A member begin function returning an iterator.
  • A begin function which can be found by argument-dependent lookup (ie: begin(range_object)) which returns an iterator.

So if you want to make a type into a range without adding members, you must use the latter: put a begin function in the same namespace as the type in question. Also, it needs to return a type that can be verified to be an iterator.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • It's undefined behaviour, not an error, to add to `namespace std` (outside the permitted exceptions), so "no error" isn't *wrong* on the part of the compiler – Caleth Apr 05 '23 at 14:53
  • @Caleth: The error part is about `ranges::begin` not being a function but an object. There should have been an error when you tried to overload it. – Nicol Bolas Apr 05 '23 at 15:01
  • 2
    `ranges::begin` is defined in a unspecified inline namespace, the definitions could coexist outside of `std`. – Caleth Apr 05 '23 at 15:17
2

You are not allowed to add overloads in the std namespace.

If C2 itself does not contain begin()/end() member functions, you can add a free function named begin()/end() under the same namespace to obtain its iterator and sentinel, making it model the range concept, for example:

struct C2 {};

int* begin(C2&);
int* end(C2&);

static_assert(std::ranges::range<C2>);

(A similar example in the standard is directory_iterator, which has free function begin()/end() overloads)

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • -*You are not allowed to add overloads in the std namespace.* Not exactly; to provide formatting for your own class, you need to specialize `std::formatter`. – Red.Wave Apr 05 '23 at 15:39
  • 1
    @Red.Wave Specializing `std::formatter` is not adding overloads to the std namespace. – Barry Apr 05 '23 at 15:47
0

@Nicol Bolas, @Caleth, @康桓瑋: Thanks for your answers and explanations. I really need to fully grasp how these customization points really work.

You're telling me that begin() is supposed to return an iterator. But again, I don't see that requirement in the definition of concept range. I'd expect to find something along the lines of:

template <class _Rng>
concept range = requires(_Rng& __r) {
    { begin(__r) } -> std::input_or_output_iterator;
    { end(__r) } -> std::input_or_output_iterator;
};

But at least the documentation, you referred me to, states this requirement.

After recursively digging into concept std::input_or_output_iterator, I managed to define a class that satisfies this concept.

For reference, this is a minimal implementation that satisfies all requirements. See Compiler Explorer:

#include <ranges>

struct V {};

struct C
{
    struct I 
    {
        using difference_type = std::ptrdiff_t;
        using value_type = V*;
        const value_type operator*() const;
        I& operator++();
        I operator++(int);
        // This operator makes C::I a valid iterator for std::ranges::range<>
        friend bool operator==(const I&, const I&);
    };

    I begin();
    I end();
};

static_assert(std::input_or_output_iterator<C::I>);

// This assertion requires operator I::operator==() to be defined.
static_assert(std::ranges::range<C>);

int main() {}

At some point, I found a hint that friend bool operator==(const I&, const I&); is required, too. But I forgot to note down the source.

Frank Heimes
  • 100
  • 6