std::ranges::begin和std::begin有什么区别?

19

使用 std::begin 和新的 std::ranges::begin 有什么区别呢?(对于endsize等也是一样的。)

两者似乎工作方式相同:

#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);
}

编译并打印输出1111,与预期相符。

ranges::begin 是否取代了 std::begin?还是两者具有不同的用例?

1个回答

33

有一些区别。

首先,ranges::begin(x) 适用于所有范围,而 std::begin(x) 不是。后者不会在 begin 上进行 ADL 查找,因此指定为以下范围时:

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

这不起作用,这就是为什么你必须编写类似以下内容的东西:

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

你不必使用ranges::begin进行两步操作。
其次,ranges::begin(x)更加安全。Ranges引入了一个名为“借用范围”的概念,它是一个可以安全地持有迭代器的范围。例如,vector<int>不是借用范围-因为一旦vector消失,数据也会消失。ranges::begin可以防止这种情况发生:
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

第三,ranges::beginranges::end有额外的类型检查。 ranges::begin(r)要求r.begin()begin(r)的结果模型为input_or_output_iteratorranges::end(r)要求ranges::begin(r)有效,并要求r.end()end(r)的结果模型为sentinel_for<decltype(ranges::begin(r))>。也就是说-我们从beginend得到的内容实际上是一个范围。

这意味着,例如:

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

有时候你可能拥有一个迭代器类型,这个类型可以进行递增、解引用、比较等操作,但是却没有默认构造函数。这不符合C++20中input_or_output_iterator的要求,因此ranges::begin会失败。

第四个区别是,ranges::begin是一个函数对象,而std::begin是一组重载的函数模板:

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

第五点,一些范围自定义点对象除了调用该名称函数之外,还有其他的回退行为。 std::size(r) 总是调用名为 size 的函数(除非 r 是一个原始数组)。 std::empty(r) 总是调用名为 empty 的函数(除非 r 是一个原始数组,在这种情况下它只是 false,或者 r 是一个 initializer_list,在这种情况下 r.size() == 0)。但是,ranges::size 可以在 某些情况下 执行 ranges::end(r) - ranges::begin(r)(如果 size(r)r.size() 不存在,则作为回退),就像 ranges::empty 可以在 某些情况下 执行 ranges::size(r) == 0ranges::begin(r) == ranges::end(r) 一样。

阅读有关ranges v3中发生的所有底层内容总是一种乐趣。 - Viktor Sehr
1
那么 ranges::begin 等是否使得 std::begin 已经过时?或者在什么情况下还必须使用 std::begin 而不是 ranges::begin - cigien
@cigien 如果你的迭代器没有默认构造函数或缺少后缀递增等功能,那么 ranges::begin 会因为这些额外的类型检查而拒绝它们。 - Barry
啊,好的,我假设那样的迭代器是有效的?并且没有违反任何其他规则? - cigien
@cigien 取决于您如何定义“有效”。如果它们不可默认构造,则它们不是有效的 C++20 input_or_output_iterator - Barry
感谢您的全面回答!所以如果我理解正确,通常没有理由在新代码中使用 std::begin,对吗? 但是也许我们可以更进一步,从不使用 .begin(),因为 ranges::begin 具有借用范围检查,或者这样做会很傻?(另外不幸的是,它要冗长得多;这是 C++ 有时似乎“擅长”的事情) - SWdV

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接