使用C++ 11基于范围的for
的正确方法是什么?
应该使用什么语法? for (auto elem : container)
,
还是for (auto& elem : container)
或for (const auto& elem : container)
?
还是其他?
最佳答案
让我们开始区分观察容器中的元素
与修改它们到位。
观察元素
让我们考虑一个简单的示例:
vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)
cout << x << ' ';
上面的代码在
int
中打印元素(vector
):1 3 5 7 9
现在考虑另一种情况, vector 元素不仅是简单的整数,
但是具有自定义复制构造函数等更复杂类的实例。
// 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
正如可以从输出中读取的那样,复制构造函数调用是在基于范围的循环迭代期间进行的。
这是因为我们通过值从容器中捕获元素
(
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)
只是无法编译。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>
,并且我们想反转逻辑 bool(boolean) 状态使用以上语法的元素:
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
以优化空间的实现(每个 bool(boolean) 值是存储在一个位中,一个字节中有八个“ bool(boolean) ”位)。
因此(由于不可能返回对单个位的引用),
vector<bool>
使用所谓的“代理迭代器” 模式。“代理迭代器”是在取消引用后不会产生
普通的
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
int
,double
等),可以使用稍微简化的形式:
for (auto elem : container) // capture by value
for (auto& elem : container) // capture by (non-const) reference
std::vector<bool>
),请使用:for (auto&& elem : container) // capture by &&
当然,如果需要在循环体内创建该元素的本地副本,则按值捕获(
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)
关于c++ - 使用C++ 11基于范围的正确方法是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42470826/