【问题标题】:Iterating through an rvalue container遍历右值容器
【发布时间】:2014-03-07 22:57:25
【问题描述】:

以下代码是否会导致未定义的行为?

std::map<int, vector<int>> foo()
{
return ...
}

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

如果未定义,修复它的好方法是什么?如果我使用 c++11 range-for 循环而不是 BOOST_FOREACH 会怎样?

【问题讨论】:

  • 你觉得这里为什么会有UB?
  • 我看到类似代码中的内存损坏。但不确定是不是因为这种用法。
  • 查看sourcecode。似乎有一些宏魔法可以检测参数是否为右值,在这种情况下,它会复制参数。
  • @user2079303:不幸的是,std::map&lt;...&gt;::operator[] 返回一个左值引用(到一个临时对象中);不是 r 值;因此,map 临时的生命周期没有正确延长,我们最终得到了对下界的引用。

标签: c++ loops c++11 rvalue boost-foreach


【解决方案1】:

不幸的是,这很可能是未定义的行为。

问题是你这里有两个级别:

  1. std::map&lt;...&gt; 是一个右值,它的生命周期会一直延长到完整表达式结束
  2. std::vector&lt;int&gt;&amp; 是一个左值引用(指向一个对象),它的生命周期就是该对象的生命周期。

问题出现是因为代码(大致)扩展为:

// 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&lt;...&gt; 的生命周期将被延长以匹配__container 的生命周期,但是在这种情况下我们得到:

// non-standard gcc extension, very handy to model temporaries:
std::vector<int>& __container = { std::map<...> m = foo(); m[42] };

因此__container 最终指向了下界。

【讨论】:

  • 你能肯定地说它总是未定义的吗?
  • @balki:不,尽管我几乎没有怀疑。据我所知,可以改进 range-for 来处理这个问题——因为标准中呈现的转换是非规范的,并且编译器知道所有存在的临时变量。但是我现在才看到 Boost 如何处理这个问题,因为类型系统的表达能力不足以反映这个问题。
  • 6.5.4/1 中的转换是规范的,除了仅用于说明的变量__range__begin__end 的名称。我不相信编译器可以在这里删除未定义的行为而不必违反临时生命周期的规则 - 所以它充其量只是一个不合格的扩展。似乎需要更改核心语言才能将 &lt;expr&gt; 中创建的任何临时对象的生命周期延长到整个循环体。
  • @Casey:啊,谢谢你的澄清。是的,显然延长任何临时的生命周期都必须规范,否则会出现可移植性问题。
【解决方案2】:

返回值一直存在到完整表达式的结尾 创建它。所以这一切都取决于BOOST_FOREACH 展开;如果它在 for 循环之外创建了一个范围,并且 将返回值复制到其中的变量(或使用它来 初始化一个引用),那么你就安全了。如果没有, 你不是。

C++11 range-for循环基本具备绑定的语义 到经典 for 循环之外的范围内的引用,所以它 应该是安全的。

编辑:

如果您要捕获的返回值,这将适用 foo。正如本杰明林德利指出的那样,你不是。你是 在地图上捕获operator[] 的返回值。和这个 不是暂时的;这是一个参考。所以没有扩展 生命周期发生在 BOOST_FOREACH 和 range-for 中。 这意味着地图本身将在结束时被破坏 包含函数调用的完整表达式,并且 发生未定义的行为。 (我想,Boost 可以使 地图的副本,这样你就安全了。但不知何故,我怀疑它 确实。)

编辑结束:

无论如何,我会质疑返回一个 std::map 当你想要的只是其中的一个条目时。如果 map 实际上存在于函数之外(不在堆上), 然后我会返回对它的引用。否则,我会找到一些 它做了什么。

【讨论】:

  • 你确定它应该在 range-for 循环中工作吗? coliru.stacked-crooked.com/a/165d826532b7eb84
  • @BenjaminLindley 该标准似乎确实如此。在第 6.5.4 节中,它给出了基于范围的等效代码,并且在那里,初始化表达式绑定到一个引用,因此它的生命周期应该延长以匹配引用的生命周期。 (当然,鉴于这是一项新功能,我不相信任何编译器都能做到 100% 正确。)
  • @BenjaminLindley 但是正如您在对我刚刚阅读的另一个答案的评论中指出的那样,他没有使用函数的返回值初始化引用;他正在使用地图上的[] 的返回值对其进行初始化。并且由于这返回一个引用(而不是一个临时的),因此没有延长生命周期。发现得好;我错过了。
【解决方案3】:

发件人:http://www.boost.org/doc/libs/1_55_0/doc/html/foreach.html

遍历按值返回序列的表达式(即右值):

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;
}

【讨论】:

  • 他做的事情可没那么简单。地图上的operator[] 不是按值返回,而是返回一个引用。
猜你喜欢
  • 1970-01-01
  • 2010-10-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多