问题描述
像如今的许多人一样,我一直在尝试 C++11 带来的不同功能.我最喜欢的方法之一是基于范围的 for 循环".
Like many people these days I have been trying the different features that C++11 brings. One of my favorites is the "range-based for loops".
我明白:
for(Type& v : a) { ... }
相当于:
for(auto iv = begin(a); iv != end(a); ++iv)
{
Type& v = *iv;
...
}
对于标准容器,begin()
只返回 a.begin()
.
And that begin()
simply returns a.begin()
for standard containers.
但是如果我想让我的自定义类型基于范围的 for 循环"感知怎么办?
我应该专门研究 begin()
和 end()
吗?
Should I just specialize begin()
and end()
?
如果我的自定义类型属于命名空间 xml
,我应该定义 xml::begin()
还是 std::begin()
?
If my custom type belongs to the namespace xml
, should I define xml::begin()
or std::begin()
?
简而言之,这样做的指导方针是什么?
In short, what are the guidelines to do that?
推荐答案
自从问题(和大多数答案)发布后,标准已经改变 本缺陷报告的解决方案.
The standard has been changed since the question (and most answers) were posted in the resolution of this defect report.
使 for(:)
循环在您的类型 X
上工作的方法现在是以下两种方法之一:
The way to make a for(:)
loop work on your type X
is now one of two ways:
创建成员
X::begin()
和X::end()
返回一些类似于迭代器的东西
Create member
X::begin()
andX::end()
that return something that acts like an iterator
创建一个自由函数 begin(X&)
和 end(X&)
返回一些类似于迭代器的东西,在与你的相同的命名空间中输入 X
.¹
Create a free function begin(X&)
and end(X&)
that return something that acts like an iterator, in the same namespace as your type X
.¹
const
变体也类似.这将适用于实施缺陷报告更改的编译器和未实施的编译器.
And similar for const
variations. This will work both on compilers that implement the defect report changes, and compilers that do not.
返回的对象实际上不必是迭代器.for(:)
循环与 C++ 标准的大多数部分不同,它是 指定扩展为等价于:
The objects returned do not have to actually be iterators. The for(:)
loop, unlike most parts of the C++ standard, is specified to expand to something equivalent to:
for( range_declaration : range_expression )
变成:
{
auto && __range = range_expression ;
for (auto __begin = begin_expr,
__end = end_expr;
__begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
其中以 __
开头的变量仅用于说明,而 begin_expr
和 end_expr
是调用 begin的魔法code>/
end
.²
where the variables beginning with __
are for exposition only, and begin_expr
and end_expr
is the magic that calls begin
/end
.²
begin/end 返回值的要求很简单:必须重载 pre-++
,确保初始化表达式有效,二进制 !=
可以在布尔上下文中使用,一元 *
返回一些你可以分配初始化 range_declaration
的东西,并公开一个公共析构函数.
The requirements on the begin/end return value are simple: You must overload pre-++
, ensure the initialization expressions are valid, binary !=
that can be used in a boolean context, unary *
that returns something you can assign-initialize range_declaration
with, and expose a public destructor.
以与迭代器不兼容的方式执行此操作可能是个坏主意,因为如果您这样做,C++ 的未来迭代可能会相对不屑一顾地破坏您的代码.
Doing so in a way that isn't compatible with an iterator is probably a bad idea, as future iterations of C++ might be relatively cavalier about breaking your code if you do.
顺便说一句,标准的未来修订版很可能允许 end_expr
返回与 begin_expr
不同的类型.这是有用的,因为它允许延迟结束".评估(如检测空终止),易于优化,与手写 C 循环一样高效,以及其他类似优点.
As an aside, it is reasonably likely that a future revision of the standard will permit end_expr
to return a different type than begin_expr
. This is useful in that it permits "lazy-end" evaluation (like detecting null-termination) that is easy to optimize to be as efficient as a hand-written C loop, and other similar advantages.
¹ 请注意,for(:)
循环将任何临时变量存储在 auto&&
变量中,并将其作为左值传递给您.您无法检测是否正在迭代临时(或其他右值);for(:)
循环不会调用这样的重载.参见 n4527 的 [stmt.ranged] 1.2-1.3.
¹ Note that for(:)
loops store any temporary in an auto&&
variable, and pass it to you as an lvalue. You cannot detect if you are iterating over a temporary (or other rvalue); such an overload will not be called by a for(:)
loop. See [stmt.ranged] 1.2-1.3 from n4527.
² 要么调用 begin
/end
方法,要么只用 ADL 查找自由函数 begin
/end
>、 或 C 风格数组支持的魔法.注意 std::begin
不会被调用,除非 range_expression
返回 namespace std
中类型的对象或依赖于相同的对象.
² Either call the begin
/end
method, or ADL-only lookup of free function begin
/end
, or magic for C-style array support. Note that std::begin
is not called unless range_expression
returns an object of type in namespace std
or dependent on same.
在c++17 表达式范围已更新
{
auto && __range = range_expression ;
auto __begin = begin_expr;
auto __end = end_expr;
for (;__begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
__begin
和 __end
的类型已经解耦了.
with the types of __begin
and __end
have been decoupled.
这允许结束迭代器与开始的类型不同.您的最终迭代器类型可以是哨兵"类型.它只支持 !=
和 begin 迭代器类型.
This permits the end iterator to not be the same type as begin. Your end iterator type can be a "sentinel" which only supports !=
with the begin iterator type.
为什么这很有用的一个实际例子是,您的最终迭代器可以读取检查您的 char*
以查看它是否指向 '0'
";当 ==
带有 char*
时.这允许 C++ range-for 表达式在迭代以空字符结尾的 char*
缓冲区时生成最佳代码.
A practical example of why this is useful is that your end iterator can read "check your char*
to see if it points to '0'
" when ==
with a char*
. This allows a C++ range-for expression to generate optimal code when iterating over a null-terminated char*
buffer.
struct null_sentinal_t {
template<class Rhs,
std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
>
friend bool operator==(Rhs const& ptr, null_sentinal_t) {
return !*ptr;
}
template<class Rhs,
std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
>
friend bool operator!=(Rhs const& ptr, null_sentinal_t) {
return !(ptr==null_sentinal_t{});
}
template<class Lhs,
std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
>
friend bool operator==(null_sentinal_t, Lhs const& ptr) {
return !*ptr;
}
template<class Lhs,
std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
>
friend bool operator!=(null_sentinal_t, Lhs const& ptr) {
return !(null_sentinal_t{}==ptr);
}
friend bool operator==(null_sentinal_t, null_sentinal_t) {
return true;
}
friend bool operator!=(null_sentinal_t, null_sentinal_t) {
return false;
}
};
活生生的例子.
最少的测试代码是:
struct cstring {
const char* ptr = 0;
const char* begin() const { return ptr?ptr:""; }// return empty string if we are null
null_sentinal_t end() const { return {}; }
};
cstring str{"abc"};
for (char c : str) {
std::cout << c;
}
std::cout << "
";
这是一个简单的例子.
Here is a simple example.
namespace library_ns {
struct some_struct_you_do_not_control {
std::vector<int> data;
};
}
您的代码:
namespace library_ns {
int* begin(some_struct_you_do_not_control& x){ return x.data.data(); }
int* end(some_struct_you_do_not_control& x){ return x.data.data()+x.data.size(); }
int const* cbegin(some_struct_you_do_not_control const& x){ return x.data.data(); }
int* cend(some_struct_you_do_not_control const& x){ return x.data.data()+x.data.size(); }
int const* begin(some_struct_you_do_not_control const& x){ return cbegin(x); }
int const* end(some_struct_you_do_not_control const& x){ return cend(x); }
}
这是一个示例,您可以如何将无法控制的类型扩充为可迭代的.
this is an example how you can augment a type you don't control to be iterable.
这里我返回指针作为迭代器,隐藏了我在引擎盖下有一个向量的事实.
Here I return pointers-as-iterators, hiding the fact I have a vector under the hood.
对于您拥有的类型,您可以添加方法:
For a type you do own, you can add methods:
struct egg {};
struct egg_carton {
auto begin() { return eggs.begin(); }
auto end() { return eggs.end(); }
auto cbegin() const { return eggs.begin(); }
auto cend() const { return eggs.end(); }
auto begin() const { return eggs.begin(); }
auto end() const { return eggs.end(); }
private:
std::vector<egg> eggs;
};
这里我重用了 vector
的迭代器.为简洁起见,我使用 auto
;在 c++11 我必须更详细.
here I reuse the vector
's iterators. I use auto
for brevity; in c++11 I'd have to be more verbose.
这是一个快速而肮脏的可迭代范围视图:
Here is a quick and dirty iterable range-view:
template<class It>
struct range_t {
It b, e;
It begin() const { return b; }
It end() const { return e; }
std::size_t size() const { return end()-begin(); }
bool empty() const { return begin()==end(); }
range_t without_back( std::size_t n = 1 ) const {
n = (std::min)(n, size());
return {begin(), end()-n};
}
range_t without_front( std::size_t n = 1 ) const {
n = (std::min)(n, size());
return {begin()+n, end()};
}
decltype(auto) front() const { return *begin(); }
decltype(auto) back() const { return *(std::prev(end())); }
};
template<class C>
auto make_range( C&& c ) {
using std::begin; using std::end;
return range_t{ begin(c), end(c) };
}
使用 c++17 模板类推导.
std::vector<int> v{1,2,3,4,5};
for (auto x : make_range(v).without_front(2) ) {
std::cout << x << "
";
}
打印 3 4 5,跳过第一个 2.
prints 3 4 5, skipping first 2.
这篇关于如何使我的自定义类型与“基于范围的 for 循环"一起使用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!