C++11/14 range-for 被过度约束...
WG21 论文是P0184R0,其动机如下:
现有的基于范围的 for 循环受到过度约束。结束
迭代器永远不会递增、递减或取消引用。要求
它是一个迭代器没有任何实际用途。
从您发布的 Standardese 中可以看出,范围的 end 迭代器仅用于循环条件 __begin != __end;。因此end 只需要与begin 相等即可,不需要可解引用或可递增。
...这会扭曲 operator== 的定界迭代器。
那么这有什么缺点呢?好吧,如果你有一个标记分隔的范围(C 字符串、文本行等),那么你必须将循环条件硬塞到迭代器的 operator== 中,基本上就像这样
#include <iostream>
template <char Delim = 0>
struct StringIterator
{
char const* ptr = nullptr;
friend auto operator==(StringIterator lhs, StringIterator rhs) {
return lhs.ptr ? (rhs.ptr || (*lhs.ptr == Delim)) : (!rhs.ptr || (*rhs.ptr == Delim));
}
friend auto operator!=(StringIterator lhs, StringIterator rhs) {
return !(lhs == rhs);
}
auto& operator*() { return *ptr; }
auto& operator++() { ++ptr; return *this; }
};
template <char Delim = 0>
class StringRange
{
StringIterator<Delim> it;
public:
StringRange(char const* ptr) : it{ptr} {}
auto begin() { return it; }
auto end() { return StringIterator<Delim>{}; }
};
int main()
{
// "Hello World", no exclamation mark
for (auto const& c : StringRange<'!'>{"Hello World!"})
std::cout << c;
}
Live Example 使用 g++ -std=c++14,(assembly 使用 gcc.godbolt.org)
上面的operator== 与StringIterator<> 的参数是对称的,并且不依赖于range-for 是begin != end 还是end != begin(否则你可以作弊并将代码切成两半)。
对于简单的迭代模式,编译器能够优化operator== 内部的复杂逻辑。实际上,对于上面的示例,operator== 被简化为单个比较。但这会继续适用于范围和过滤器的长管道吗?谁知道。它可能需要英雄优化级别。
C++17 将放宽限制以简化分隔范围...
那么简化究竟体现在哪里?在operator== 中,它现在有额外的重载,采用迭代器/哨兵对(为了对称,在两个顺序中)。所以运行时逻辑变成了编译时逻辑。
#include <iostream>
template <char Delim = 0>
struct StringSentinel {};
struct StringIterator
{
char const* ptr = nullptr;
template <char Delim>
friend auto operator==(StringIterator lhs, StringSentinel<Delim> rhs) {
return *lhs.ptr == Delim;
}
template <char Delim>
friend auto operator==(StringSentinel<Delim> lhs, StringIterator rhs) {
return rhs == lhs;
}
template <char Delim>
friend auto operator!=(StringIterator lhs, StringSentinel<Delim> rhs) {
return !(lhs == rhs);
}
template <char Delim>
friend auto operator!=(StringSentinel<Delim> lhs, StringIterator rhs) {
return !(lhs == rhs);
}
auto& operator*() { return *ptr; }
auto& operator++() { ++ptr; return *this; }
};
template <char Delim = 0>
class StringRange
{
StringIterator it;
public:
StringRange(char const* ptr) : it{ptr} {}
auto begin() { return it; }
auto end() { return StringSentinel<Delim>{}; }
};
int main()
{
// "Hello World", no exclamation mark
for (auto const& c : StringRange<'!'>{"Hello World!"})
std::cout << c;
}
Live Example 使用 g++ -std=c++1z(assembly 使用 gcc.godbolt.org,几乎与前面的示例相同)。
...实际上将支持完全通用的原始“D 样式”范围。
WG21论文N4382有以下建议:
C.6 Range Facade 和适配器实用程序 [future.facade]
1 直到它
用户创建自己的迭代器类型变得微不足道,完整的
迭代器的潜力将仍未实现。范围抽象
使之成为可能。使用正确的库组件,它应该是
用户可以用最小的接口定义一个范围(例如,
current、done 和 next 成员),并具有迭代器类型
自动生成。这样的范围外观类模板保留为
未来的工作。
本质上,这等于 D 样式范围(其中这些原语称为 empty、front 和 popFront)。只有这些原语的分隔字符串范围看起来像这样:
template <char Delim = 0>
class PrimitiveStringRange
{
char const* ptr;
public:
PrimitiveStringRange(char const* c) : ptr{c} {}
auto& current() { return *ptr; }
auto done() const { return *ptr == Delim; }
auto next() { ++ptr; }
};
如果不知道原始范围的底层表示,如何从中提取迭代器?如何使其适应可与 range-for 一起使用的范围?这是一种方法(另见@EricNiebler 的series of blog posts)和来自@T.C. 的cmets:
#include <iostream>
// adapt any primitive range with current/done/next to Iterator/Sentinel pair with begin/end
template <class Derived>
struct RangeAdaptor : private Derived
{
using Derived::Derived;
struct Sentinel {};
struct Iterator
{
Derived* rng;
friend auto operator==(Iterator it, Sentinel) { return it.rng->done(); }
friend auto operator==(Sentinel, Iterator it) { return it.rng->done(); }
friend auto operator!=(Iterator lhs, Sentinel rhs) { return !(lhs == rhs); }
friend auto operator!=(Sentinel lhs, Iterator rhs) { return !(lhs == rhs); }
auto& operator*() { return rng->current(); }
auto& operator++() { rng->next(); return *this; }
};
auto begin() { return Iterator{this}; }
auto end() { return Sentinel{}; }
};
int main()
{
// "Hello World", no exclamation mark
for (auto const& c : RangeAdaptor<PrimitiveStringRange<'!'>>{"Hello World!"})
std::cout << c;
}
Live Example 使用 g++ -std=c++1z(assembly 使用 gcc.godbolt.org)
结论:哨兵不仅仅是一种将分隔符压入类型系统的可爱机制,它们对于support primitive "D-style" ranges(它们本身可能没有迭代器的概念)作为零开销来说足够通用新的 C++1z range-for 的抽象。