C++ range based for循环中的生命周期
在C++中,Range-based for循环(range-based for loop)是一种简化遍历容器和数组元素的方法。然而,当处理生命周期和临时对象时,可能会遇到一些问题。在本文中,我们将讨论Range-based for循环中的生命周期问题。
正确写法
#include <stdio.h>
#include <initializer_list>
#include <utility>
int main() {
for(int i : {1, 3, 2}) {
printf("%d\n", i);
}
}
这段代码会被编译器转换为:
int main() {
auto && __range = {1, 3, 2};
auto __begin = std::begin(__range);
auto __end = std::end(__range);
for(__begin != __end; ++__begin) {
printf("%d\n", * __begin);
}
}
其中 auto && __range = {1, 3, 2};
的auto
被推断为 std::initializer_list<int>
。
{1,2,3}
的生命周期相同被延长到与__range
相同。
错误写法 1
int main() {
for(int i : std::move({1, 3, 2}) ) {
printf("%d\n", i);
}
}
这段代码会被编译器转换为:
int main() {
auto && __range = std::move({1, 3, 2});
...
}
然而,std::move()
是一个模板函数。
编译器无法区分这是使用三个数字来初始化一个变量,还是用一个 initializer_list
来初始化一个变量。
因此,上面的代码会导致编译错误。
错误写法2
int main() {
for(int i : std::move(std::initializer_list<int>({1, 3, 2})) ) {
printf("%d\n", i);
}
}
这段代码会被编译器转换为:
int main() {
auto && __range = std::move(std::initializer_list<int>({1, 3, 2})); # 1
...
}
我们来看 std::move()
的实现:
template<class T>
std::remove_reference_t<T> &&std::move(T &&v) {
return static_cast<std::remove_reference_t<T>&&>(v);
}
返回的是引用。
所以__range
是对{1,3,2}
的引用。但是显然作为std::move()
的参数,它的生命周期在语句#1之后就结束了。
因为由于__range
引用的对象不存在了,程序的结果是错误的。
参考 https://godbolt.org/z/PMTd4P 。
思考
下面的代码为什么是错误的:
struct Foo {
std::vector<int> &&items() {
return m_items;
}
std::vector<int> m_items;
};
Foo foo() { return {}; }
for (auto& x : foo().items()) {
}
答案:
foo().items()
返回一个指向 m_items
的引用。
由于 foo()
返回一个临时对象,当 foo().items()
被调用结束时,
该临时对象已经超出了其生命周期。
因此,返回的引用指向了一个不存在的对象,
导致未定义行为。
注:
在C++23中,整个 range-expression
的所有临时变量的生命周期都会拓展到整个for循环结束。
这意味着标准委员会已经认识到了这种易犯错误的情况,并对其进行了优化。
在新标准下,程序员将更容易避免这类错误,从而编写更安全、更健壮的代码。