什么是正确的方式使用C + + 11的基于范围的?
什么是正确的方式使用C + + 11的基于范围的?
应该使用什么语法? 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 << ' ';
上面的代码打印vector
的元素( int
s):
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 : v)
的auto x
部分) for (auto x : v)
从容器中捕获元素。
这是低效率的代码,例如,如果这些元素是std::string
实例,那么堆内存分配可以完成,而且内存pipe理器的代价很高。如果我们只想观察容器中的元素,那么这是没用的。
所以,更好的语法是可用的: 通过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
没有任何虚假(可能是昂贵的)复制构造函数调用。
因此,当观察容器中的元素(即只读访问)时,对于简单廉价的复制types(如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)
语法,这也适用于更复杂的types,例如考虑一个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
优化空间(每个布尔值存储在一个位,一个字节8个“布尔”位)。
因为(因为不可能返回一个位的引用), 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)
语法也适用于其他普通(非代理)迭代器(例如,对于一个vector<int>
或一个vector<string>
)的情况。
(作为一个方面说明, for (const auto& elem : container)
的上述“观察”语法对于代理迭代器的情况也可以正常工作。)
概要
上述讨论可以总结为以下指导原则:
-
要观察元素,请使用以下语法:
for (const auto& elem : container) // capture by const reference
-
如果对象复制便宜 (如
int
s,double
s等),则可以使用稍微简化的forms: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 &&
-
当然,如果需要在循环体内创build元素的本地副本 ,则通过值 ( for (auto elem : container)
) for (auto elem : container)
捕获是个不错的select。
有关通用代码的其他说明
在通用代码中 ,由于我们不能对通用typesT
进行复制便宜的假设,所以在观察模式下总是使用for (const auto& elem : container)
。
(这不会触发潜在的昂贵的无用拷贝,对于廉价的拷贝types,比如int
,对于使用代理迭代器的容器也是如此,比如std::vector<bool>
。
而且,在修改模式下,如果我们希望generics代码也可以在代理迭代器的情况下工作,最好的select是for (auto&& elem : container)
。
(对于使用普通非代理迭代器的容器,这也可以正常工作,如std::vector<int>
或std::vector<string>
。)
因此,在通用代码中 ,可以提供以下准则:
-
观察元素,请使用:
for (const auto& elem : container)
-
要修改元素,请使用:
for (auto&& elem : container)
for (auto elem : container)
, for (auto& elem : container)
或for (const auto& elem : container)
没有正确的方法 。 你只是expression你想要的。
让我详细说明一下。 让我们散步吧。
for (auto elem : container) ...
这是一个语法糖:
for(auto it = container.begin(); it != container.end(); ++it) { // Observe that this is a copy by value. auto elem = *it; }
如果您的容器包含便宜的元素,则可以使用这个元素。
for (auto& elem : container) ...
这是一个语法糖:
for(auto it = container.begin(); it != container.end(); ++it) { // Now you're directly modifying the elements // because elem is an lvalue reference auto& elem = *it; }
例如,当您想要直接写入容器中的元素时,请使用它。
for (const auto& elem : container) ...
这是一个语法糖:
for(auto it = container.begin(); it != container.end(); ++it) { // You just want to read stuff, no modification const auto& elem = *it; }
正如评论所说,只是为了阅读。 就是这样,正确使用时一切都是“正确的”。
正确的手段总是
for(auto&& elem : container)
这将保证所有语义的保存。