19

What is the difference between std::begin and the new std::ranges::begin? (same for end, size, etc.)

Both seem to work identically:

#include <iostream>
#include <vector>
#include <array>
#include <ranges>

template<std::ranges::range R>
void printInfo(const R &range)
{
    std::cout << (std::ranges::begin(range) == std::begin(range));
}

template<class T>
struct X
{
    std::vector<T> v;

    auto begin() const { return v.begin(); }
    auto end() const { return v.end(); }
};

int main()
{
    printInfo(std::vector{1, 2, 3, 4});
    printInfo(std::array{1, 2, 3, 4});
    printInfo(X<int>{{1, 2, 3, 4}});

    int oldSchool[]{1, 2, 3, 4};
    printInfo(oldSchool);
}

Compiles and prints 1111, as expected.

Does ranges::begin make std::begin obsolete? Or do the two have different use-cases?

SWdV
  • 1,715
  • 1
  • 15
  • 36

1 Answers1

32

There are a few differences.

First, ranges::begin(x) works on all ranges while std::begin(x) does not. The latter will not do ADL lookup on begin, so ranges specified like:

struct R {
    ...
};
auto begin(R const&);
auto end(R const&);

won't work, which is why you have to write something like:

using std::begin, std::end;
auto it = begin(r);

You don't have to do that two-step with ranges::begin.

Second, ranges::begin(x) is a little safer. Ranges introduces this notion of a borrowed range, which is a range whose iterators that you can hold onto safely. vector<int> for instance is not a borrowed range - since once the vector dies the data dies. ranges::begin guards against that:

auto get_data() -> std::vector<int>;

auto a = std::begin(get_data());    // ok, but now we have a dangling iterator
auto b = ranges::begin(get_data()); // ill-formed

Third, ranges::begin and ranges::end have extra type checks. ranges::begin(r) requires the result of either r.begin() or begin(r) to model input_or_output_iterator. ranges::end(r) requires ranges::begin(r) to be valid and requires either r.end() or end(r) to model sentinel_for<decltype(ranges::begin(r))>. That is - that whatever we get from begin and end is actually a range.

This means that, for instance:

struct X {
    int begin() const { return 42; }
};

X x;
auto a = std::begin(x);    // ok, a == 42
auto b = ranges::begin(x); // ill-formed, int is not an iterator

Although more annoyingly is a case where you have an iterator type that might be incrementable, dereferenceable, comparable, etc... but fail to have a default constructor. That does not meet the requirements of C++20's input_or_output_iterator so ranges::begin will fail.

Fourth, ranges::begin is a function object, while std::begin is a set of overloaded function templates:

auto f = ranges::begin; // ok
auto g = std::begin;    // error: which std::begin did you want?

Fifth, some of the ranges customization point objects have other fallback behavior besides just calling a function of that name. std::size(r) always invokes a function named size (unless r is a raw array). std::empty(r) always invokes a function named empty(unless r is a raw array, in which case it's just false, or r is an initializer_list, in which case r.size() == 0). But ranges::size could under certain circumstances perform ranges::end(r) - ranges::begin(r) (as a fallback if size(r) and r.size() don't exist) just like ranges::empty could under certain circumstances either do ranges::size(r) == 0 or ranges::begin(r) == ranges::end(r).

Barry
  • 286,269
  • 29
  • 621
  • 977
  • It's always a pleasure to read about all the under-the-hood stuff happening in ranges v3. – Viktor Sehr Jun 03 '20 at 22:06
  • 1
    So `ranges::begin`, etc does make `std::begin` obsolete? Or are there cases where one still must use `std::begin` instead of `ranges::begin`? – cigien Jun 03 '20 at 22:38
  • @cigien Well, if your iterators aren't default constructible or lack postfix increment or something, then `ranges::begin` will reject them due to those extra type checks. – Barry Jun 03 '20 at 22:41
  • Aah, ok, and I'm assuming that iterators like that are valid? and not breaking any other rules? – cigien Jun 03 '20 at 22:42
  • @cigien Depends on how you define "valid." They're not valid C++20 `input_or_output_iterator`s if they're not default constructible. – Barry Jun 03 '20 at 22:46
  • Thanks for your comprehensive answer! So if I understand it correctly there would generally be no reason to use `std::begin` in new code, right? But then maybe we could take it a step further and also never use `.begin()` because `ranges::begin` has the borrowed range check, or would that be silly? (Also unfortunately it's much more verbose; something which C++ seems to be 'good' at sometimes) – SWdV Jun 04 '20 at 17:38