Range-based
for
loop
(since C++11)
在某个范围内执行 for 循环。
作为传统 for 循环 的更可读替代方案,用于遍历一定范围内的值,例如容器中的所有元素。
目录 |
语法
attr
(可选)
for (
init-statement
(可选)
item-declaration
:
range-initializer
)
statement
|
|||||||||
| attr | - | 任意数量的 属性 | ||
| init-statement | - |
(自 C++20 起)
以下之一
注意:任何 init-statement 都必须以分号结尾。因此常被非正式地描述为表达式或声明后接分号。 |
||
| item-declaration | - | 每个范围项的声明 | ||
| range-initializer | - | 一个 表达式 或 花括号包围的初始化器列表 | ||
| statement | - | 任意 语句 (通常是复合语句) |
说明
上述语法生成的代码等效于以下内容 除了 range-initializer 临时对象的生存期扩展(参见 下文 ) (C++23 起) (包裹在 /* */ 中的变量和表达式仅用于说明):
|
|
(C++17 前) |
|
|
(C++17 起)
(C++20 前) |
|
|
(C++20 起) |
range-initializer 会被求值以初始化要迭代的序列或范围。该序列中的每个元素会依次被解引用,并用于初始化具有 item-declaration 中给定的类型和名称的变量。
item-declaration 可以是以下之一:
说明性表达式 /* begin-expr */ 与 /* end-expr */ 定义如下:
-
如果
/* range */
的类型是数组类型
R的引用:
-
-
如果
R具有边界 N ,则 /* begin-expr */ 为 /* range */ ,且 /* end-expr */ 为 /* range */ + N 。 -
如果
R是未知边界的数组或不完整类型的数组,则程序非良构。
-
如果
-
若
/* range */
的类型是对类类型
C的引用,且在C的作用域中查找名称 “begin” 和 “end” 各自找到至少一个声明,则 /* begin-expr */ 为 /* range */ . begin ( ) 且 /* end-expr */ 为 /* range */ . end ( ) 。 -
否则,
/* begin-expr */
为
begin
(
/* range */
)
且
/* end-expr */
为
end
(
/* range */
)
,其中 “
begin” 和 “end” 通过 实参依赖查找 找到(不执行非 ADL 查找)。
如果循环需要在 statement 内终止,可以使用 break 语句 作为终止语句。
若需要在 statement 中终止当前迭代,可使用 continue 语句 作为快捷方式。
如果在 init-statement 中引入的名称在 statement 的最外层块中被重新声明,则程序非良构:
for (int i : {1, 2, 3}) int i = 1; // 错误:重复声明
临时范围初始化器
如果 range-initializer 返回一个临时对象,其生命周期将延长至循环结束,这是通过绑定到转发引用 /* range */ 实现的。
所有位于 范围初始化器 内的临时对象生命周期均不会延长 ,除非它们原本就会在 范围初始化器 结束时被销毁 (C++23 起) 。
// 如果 foo() 通过值返回 for (auto& x : foo().items()) { /* ... */ } // C++23 之前是未定义行为
|
此问题可通过使用 初始化语句 解决: for (T thing = foo(); auto& x : thing.items()) { /* ... */ } // OK |
(since C++20) |
|
注意:即使在 C++23 中,中间函数调用的非引用参数也不会获得生命周期扩展(因为在某些 ABI 中它们在被调用方而非调用方中被销毁),但这仅对本身存在缺陷的函数构成问题: using T = std::list<int>; const T& f1(const T& t) { return t; } const T& f2(T t) { return t; } // always returns a dangling reference T g(); void foo() { for (auto e : f1(g())) {} // OK: lifetime of return value of g() extended for (auto e : f2(g())) {} // UB: lifetime of f2's value parameter ends early } |
(since C++23) |
注释
如果 range-initializer 是一个 大括号包围的初始化列表 , /* range */ 会被推导为 std::initializer_list 的引用。
在泛型代码中使用推导至转发引用是安全的,实际上更可取, for ( auto && var : sequence ) 。
当范围类型拥有名为“
begin
”和“
end
”的成员时,将采用成员解释方式。此规则与成员是类型、数据成员、函数还是枚举项无关,也与其可访问性无关。因此,即使存在命名空间作用域的“
begin
”/“
end
”函数,类似
class
meow
{
enum
{
begin
=
1
, end
=
2
}
;
/* 类其余部分 */
}
;
这样的类也无法用于基于范围的
for
循环。
虽然在 item-declaration 中声明的变量通常会在 statement 中使用,但并非必须如此。
|
自 C++17 起, /* begin-expr */ 与 /* end-expr */ 的类型不必相同,实际上 /* end-expr */ 的类型甚至不必是迭代器:只需能够与迭代器进行不等性比较即可。这使得通过谓词(例如“迭代器指向空字符时”)界定范围成为可能。 |
(since C++17) |
当与具有写时复制语义的(非const)对象一起使用时,基于范围的
for
循环可能通过(隐式)调用非const
begin()
成员函数触发深拷贝。
|
若此行为不符合预期(例如由于循环实际上并未修改对象),可通过使用 std::as_const 避免: struct cow_string { /* ... */ }; // a copy-on-write string cow_string str = /* ... */; // for (auto x : str) { /* ... */ } // may cause deep copy for (auto x : std::as_const(str)) { /* ... */ } |
(since C++17) |
| 功能测试宏 | 值 | 标准 | 功能特性 |
|---|---|---|---|
__cpp_range_based_for
|
200907L
|
(C++11) | 基于范围的 for 循环 |
201603L
|
(C++17) |
具有不同
begin
/
end
类型的基于范围
for
循环
|
|
202211L
|
(C++23) | 范围初始化器 中所有临时对象的生存期扩展 |
关键词
示例
#include <iostream> #include <vector> int main() { std::vector<int> v = {0, 1, 2, 3, 4, 5}; for (const int& i : v) // 通过常量引用访问 std::cout << i << ' '; std::cout << '\n'; for (auto i : v) // 通过值访问,i的类型为int std::cout << i << ' '; std::cout << '\n'; for (auto&& i : v) // 通过转发引用访问,i的类型为int& std::cout << i << ' '; std::cout << '\n'; const auto& cv = v; for (auto&& i : cv) // 通过转发引用访问,i的类型为const int& std::cout << i << ' '; std::cout << '\n'; for (int n : {0, 1, 2, 3, 4, 5}) // 初始化器可以是 // 花括号包围的初始化列表 std::cout << n << ' '; std::cout << '\n'; int a[] = {0, 1, 2, 3, 4, 5}; for (int n : a) // 初始化器可以是数组 std::cout << n << ' '; std::cout << '\n'; for ([[maybe_unused]] int n : a) std::cout << 1 << ' '; // 循环变量不需要被使用 std::cout << '\n'; for (auto n = v.size(); auto i : v) // 初始化语句(C++20) std::cout << --n + i << ' '; std::cout << '\n'; for (typedef decltype(v)::value_type elem_t; elem_t i : v) // 类型定义声明作为初始化语句(C++20) std::cout << i << ' '; std::cout << '\n'; for (using elem_t = decltype(v)::value_type; elem_t i : v) // 别名声明作为初始化语句(C++23) std::cout << i << ' '; std::cout << '\n'; }
输出:
0 1 2 3 4 5 0 1 2 3 4 5 0 1 2 3 4 5 0 1 2 3 4 5 0 1 2 3 4 5 0 1 2 3 4 5 1 1 1 1 1 1 5 5 5 5 5 5 0 1 2 3 4 5 0 1 2 3 4 5
缺陷报告
以下行为变更缺陷报告被追溯应用于先前发布的 C++ 标准。
| 缺陷报告 | 应用于 | 发布时的行为 | 正确行为 |
|---|---|---|---|
| CWG 1442 | C++11 |
未明确说明非成员
“
begin
”和“
end
”的查找是否包含常规非限定查找
|
不进行常规非限定查找 |
| CWG 2220 | C++11 | init-statement 中引入的名称可以被重新声明 | 此情况下程序格式错误 |
| CWG 2825 | C++11 |
若
range-initializer
为花括号包围的初始化列表,
将查找非成员“
begin
”和“
end
”
|
此情况下将查找成员“
begin
”
和“
end
”
|
| P0962R1 | C++11 |
只要存在成员“
begin
”
或“
end
”就采用成员解释
|
仅当两者同时存在时才采用 |
参见
|
对
范围
内的元素应用一元
函数对象
(函数模板) |