让我们开始区分观察容器中的元素与在适当位置修改它们。
观察元素
让我们考虑一个简单的示例:
vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)
cout << x << ' ';
上面的代码打印元件(int
在S) vector
:
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
,则可以完成堆内存分配,并且需要昂贵的内存管理器行程,等等。如果我们只想观察容器中的元素,那么这将毫无用处。
因此,可以使用更好的语法:通过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
无需任何虚假(且可能很昂贵)的复制构造函数调用。
因此,在观察容器中的元素时(例如,用于只读访问),以下语法适合简单的廉价复制类型,例如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)
just 的尝试无法编译。
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)
语法也适用于更复杂的类型,例如考虑a 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
,与该一个实现包的bool
s到优化空间(每个布尔值被存储在一个比特,一个字节八“布尔”比特)。
因此(由于不可能返回对单个位的引用),请
vector<bool>
使用所谓的“代理迭代器”模式。“代理迭代器”是一个迭代器,当取消引用时,它不会产生普通对象bool &
,而是(按值)返回一个临时对象,该对象是可转换为的代理类bool
。(在StackOverflow上另请参阅此问题和相关答案。)
要在位置修改的元素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)
语法在普通(非代理)迭代器的其他情况下也适用(例如,对于a vector<int>
或a 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)