遍历rvalue容器的方法

3
以下代码是否会导致未定义的行为?
std::map<int, vector<int>> foo()
{
return ...
}

BOOST_FOREACH(const int& i, foo()[42])
{
std::cout << i << std::endl;
}

如果未定义,有什么好的方法可以修复它?如果我使用C++11范围for循环而不是BOOST_FOREACH会怎样?


你为什么认为这里会有未定义行为(UB)? - zch
我在类似的代码中发现了内存损坏问题,但不确定是否是由于这样的使用方式导致的。 - balki
请查看 源代码。似乎有一些宏魔法来检测参数是否为右值,在这种情况下,它会复制该参数。 - eerorika
1
很不幸,std::map<...>::operator[] 返回一个左值引用(指向临时对象)而不是右值。因此,map 临时对象的生命周期没有得到正确延长,我们最终会得到一个指向虚无的引用。 - Matthieu M.
3个回答

3
很不幸,这很可能是未定义的行为。
问题在于你有两个级别:
1. std::map<...> 是一个 r-value,它的生命周期将会扩展到完整表达式结束。 2. std::vector<int>& 是一个 l-value 引用(指向一个对象),它的生命周期是该对象的生命周期。
问题出现在代码(大致上)扩展为以下内容:
// from
for (<init>: <expr>) {
    <body>
}

// to
auto&& __container = <expr>;
for (auto __it = begin(container), __e = end(container); __it != __e; ++__it)
{
    <init> = *__it;
    <body>
}

问题在于__container的初始化:
auto&& __container = foo()[42];

如果只是foo(),那么这将起作用,因为std::map<...>的生命周期会被延长以匹配__container的生命周期,但在这种情况下,我们得到了:
// non-standard gcc extension, very handy to model temporaries:
std::vector<int>& __container = { std::map<...> m = foo(); m[42] };

因此,__container 最终指向虚无。

你能肯定地说它总是未定义的吗? - balki
@balki:不是,尽管我有些怀疑。据我所知,范围-for循环可以进行改进来处理这个问题——因为标准中提出的转换是非规范化的,而编译器知道存在的所有临时变量。然而,我现在只看到了一种方法,即Boost如何处理这个问题,因为类型系统不足以反映这个问题。 - Matthieu M.
6.5.4/1 中的转换是规范的,除了只在表述中使用的变量 __range__begin__end 的名称。我认为编译器无法消除此处的未定义行为,而不必违反暂时对象生存期的规则 - 因此这最多只能是一种不符合标准的扩展。它似乎需要对核心语言进行更改,以将 <expr> 中创建的任何暂时对象的生存期延长到整个循环体。 - Casey
@Casey:啊,感谢澄清。是的,显然延长任何临时变量的生命周期必须被规范化,否则会存在可移植性问题。 - Matthieu M.

2
返回值存在于创建它的完整表达式结束之前。所以这完全取决于BOOST_FOREACH如何扩展;如果它在for循环之外创建了一个作用域,并将返回值复制到其中的变量中(或者使用它来初始化引用),那么就安全了。如果没有,那么就不安全。
C++11的range-for循环基本上具有绑定到经典for循环之外范围内的引用的语义,因此应该是安全的。
编辑:
如果你捕获foo的返回值,那么这将适用。正如Benjamin Lindley所指出的那样,你没有捕获map上的operator[]的返回值。这不是一个临时变量;它是一个引用。因此,在BOOST_FOREACH和range-for中都不会扩展生命周期。这意味着包含函数调用的完整表达式的末尾将销毁地图,并且将发生未定义的行为。(我想,Boost可以深度复制地图,所以你会安全。但某种程度上,我怀疑它是否这样做。)
编辑结束
尽管如此,我仍然会质疑当你只想要其中一个条目时返回一个std::map的智慧。如果地图实际上存在于函数之外(不在堆上),那么我会返回对它的引用。否则,我会找到一些方法使它存在。

你确定它应该在范围for循环中工作吗?http://coliru.stacked-crooked.com/a/165d826532b7eb84 - Benjamin Lindley
@BenjaminLindley 标准似乎确实是这样说的。在§6.5.4中,它给出了基于范围的for循环的等效代码,并且在那里,初始化表达式被绑定到一个引用上,因此它的生命周期应该延长以匹配引用的生命周期。(当然,考虑到这是一个新功能,我不会相信任何编译器已经完全正确地实现了它。) - James Kanze
@BenjaminLindley 但正如你在另一个回答的评论中指出的那样,他没有用函数的返回值来初始化引用;他是用映射表上[]的返回值来初始化它的。由于这会返回一个引用(而不是临时对象),所以不会有生命周期的延长。很好地发现了这一点,我之前没注意到。 - James Kanze

0

来自:http://www.boost.org/doc/libs/1_55_0/doc/html/foreach.html

遍历返回值为序列的表达式(即rvalue):

extern std::vector<float> get_vector_float();
BOOST_FOREACH( float f, get_vector_float() )
{
    // Note: get_vector_float() will be called exactly once
}

所以它被清晰地定义并且工作正常。

此外,在C++11中也有明确定义(并且工作正常):

for (const int& i : get_vector()) // get_vector() computed only once
{
    std::cout << i << std::endl;
}

这里的问题在于foo()[42]返回一个临时对象(通过方法),并且是一个引用
auto& v = foo()[42];

foo()的生命周期是临时的,不会被延长...

您可以通过延长foo的临时寿命来解决这个问题。

auto&& m = foo();

for (const int& i : m[42]) {
    std::cout << i << std::endl;
}

他所做的并不简单。在 map 上使用 operator[] 不会返回值,而是返回引用。 - Benjamin Lindley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接