TL;DR:考虑以下准则:
-
对于观察元素,使用以下语法:
for (const auto& elem : container) // capture by const reference
-
为了修改元素,使用:
for (auto& elem : container) // capture by (non-const) reference
当然,如果需要在循环体内制作元素的本地副本,按值捕获 (for (auto elem : container)) 是一个不错的选择.
详细讨论
让我们开始区分观察容器中的元素
vs. 修改它们。
观察元素
让我们考虑一个简单的例子:
vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)
cout << x << ' ';
以上代码打印vector中的元素(ints):
1 3 5 7 9
现在考虑另一种情况,其中向量元素不仅仅是简单的整数,
但是更复杂的类的实例,具有自定义复制构造函数等。
// A sample test class, with custom copy semantics.
class X
{
public:
X()
: m_data(0)
{}
X(int data)
: m_data(data)
{}
~X()
{}
X(const X& other)
: m_data(other.m_data)
{ cout << "X copy ctor.\n"; }
X& operator=(const X& other)
{
m_data = other.m_data;
cout << "X copy assign.\n";
return *this;
}
int Get() const
{
return m_data;
}
private:
int m_data;
};
ostream& operator<<(ostream& os, const X& x)
{
os << x.Get();
return os;
}
如果我们对这个新类使用上面的for (auto x : v) {...} 语法:
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (auto x : v)
{
cout << x << ' ';
}
输出类似于:
[... copy constructor calls for vector<X> initialization ...]
Elements:
X copy ctor.
1 X copy ctor.
3 X copy ctor.
5 X copy ctor.
7 X copy ctor.
9
由于可以从输出中读取,复制构造函数调用是在基于范围的 for 循环迭代期间进行的。
这是因为我们捕获容器中的元素按值
(auto x 部分在for (auto x : v))。
这是低效代码,例如,如果这些元素是 std::string 的实例,
可以完成堆内存分配,代价高昂地访问内存管理器等。
如果我们只想观察容器中的元素,这是没有用的。
因此,可以使用更好的语法:捕获 by const 参考,即 const auto&:
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (const auto& x : v)
{
cout << x << ' ';
}
现在的输出是:
[... copy constructor calls for vector<X> initialization ...]
Elements:
1 3 5 7 9
没有任何虚假(并且可能很昂贵)的复制构造函数调用。
因此,当观察容器中的元素时(即只读访问),
以下语法适用于简单的 cheap-to-copy 类型,例如 int、double 等:
for (auto elem : container)
否则,在一般情况下,通过const 引用捕获更好,
避免无用(并且可能很昂贵)的复制构造函数调用:
for (const auto& elem : container)
修改容器中的元素
如果我们想修改使用基于范围的for容器中的元素,
以上for (auto elem : container)和for (const auto& elem : container)
语法错误。
实际上,在前一种情况下,elem 存储了原件的副本
元素,因此对其进行的修改只是丢失并且不会持久存储
在容器中,例如:
vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v) // <-- capture by value (copy)
x *= 10; // <-- a local temporary copy ("x") is modified,
// *not* the original vector element.
for (auto x : v)
cout << x << ' ';
输出只是初始序列:
1 3 5 7 9
相反,使用for (const auto& x : v) 的尝试编译失败。
g++ 输出类似这样的错误消息:
TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
x *= 10;
^
在这种情况下,正确的方法是通过非const 引用捕获:
vector<int> v = {1, 3, 5, 7, 9};
for (auto& x : v)
x *= 10;
for (auto x : v)
cout << x << ' ';
输出是(如预期的那样):
10 30 50 70 90
for (auto& elem : container) 语法也适用于更复杂的类型,
例如考虑vector<string>:
vector<string> v = {"Bob", "Jeff", "Connie"};
// Modify elements in place: use "auto &"
for (auto& x : v)
x = "Hi " + x + "!";
// Output elements (*observing* --> use "const auto&")
for (const auto& x : v)
cout << x << ' ';
输出是:
Hi Bob! Hi Jeff! Hi Connie!
代理迭代器的特例
假设我们有一个vector<bool>,我们想要反转逻辑布尔状态
它的元素,使用上面的语法:
vector<bool> v = {true, false, false, true};
for (auto& x : v)
x = !x;
以上代码编译失败。
g++ 输出类似这样的错误信息:
TestRangeFor.cpp:168:20: error: invalid initialization of non-const reference of
type 'std::_Bit_reference&' from an rvalue of type 'std::_Bit_iterator::referen
ce {aka std::_Bit_reference}'
for (auto& x : v)
^
问题在于std::vector 模板专门为bool 使用,带有
打包 bools 以优化空间的实现(每个布尔值是
存储在一个位中,一个字节中有八个“布尔”位)。
因此(因为不可能返回对单个位的引用),
vector<bool> 使用所谓的 “代理迭代器” 模式。
“代理迭代器”是一个迭代器,当被取消引用时,not 会产生一个
普通的bool &,而是返回(按值)一个临时对象,
这是一个proxy class convertible to bool。
(另请参阅 StackOverflow 上的 this question and related answers。)
修改vector<bool>的元素,一种新的语法(使用auto&&)
必须使用:
for (auto&& x : v)
x = !x;
以下代码可以正常工作:
vector<bool> v = {true, false, false, true};
// Invert boolean status
for (auto&& x : v) // <-- note use of "auto&&" for proxy iterators
x = !x;
// Print new element values
cout << boolalpha;
for (const auto& x : v)
cout << x << ' ';
和输出:
false true true false
请注意,for (auto&& elem : container) 语法也适用于其他情况
普通(非代理)迭代器(例如 vector<int> 或 vector<string>)。
(附带说明,for (const auto& elem : container) 的上述“观察”语法也适用于代理迭代器情况。)
总结
上述讨论可以总结为以下指南:
-
对于观察元素,使用以下语法:
for (const auto& elem : container) // capture by const reference
-
为了修改元素,使用:
for (auto& elem : container) // capture by (non-const) reference
当然,如果需要在循环体内制作元素的本地副本,按值捕获(for (auto elem : container))是一个不错的选择.
关于泛型代码的附加说明
在泛型代码中,由于我们无法假设泛型类型T 复制起来很便宜,因此在观察模式下始终使用是安全的for (const auto& elem : container)。
(这不会触发潜在的昂贵的无用副本,对于像int 这样的廉价复制类型也可以正常工作,也对于使用代理迭代器的容器,如std::vector<bool>。)
此外,在修改模式下,如果我们希望通用代码在代理迭代器的情况下也能工作,最好的选择是for (auto&& elem : container)强>.
(这也适用于使用普通非代理迭代器的容器,例如 std::vector<int> 或 std::vector<string>。)
因此,在通用代码中,可以提供以下准则:
-
为了观察元素,使用:
for (const auto& elem : container)
-
为了修改元素,使用:
for (auto&& elem : container)