编译器对超出范围的变量的右值引用进行推断

Posted

技术标签:

【中文标题】编译器对超出范围的变量的右值引用进行推断【英文标题】:Compiler deduction of rvalue-references for variables going out of scope 【发布时间】:2017-08-18 19:38:33 【问题描述】:

为什么编译器不会自动推断变量即将超出范围,因此让它被视为右值引用?

以这段代码为例:

#include <string>

int foo(std::string && bob);
int foo(const std::string & bob);

int main()

    std::string bob("  ");
    return foo(bob);

检查汇编代码清楚地表明,“foo”的 const & 版本在函数末尾被调用。

编译器资源管理器链接在这里:https://godbolt.org/g/mVi9y6

编辑:澄清一下,我不是在寻找有关移动变量的替代方法的建议。我也不是想理解为什么编译器选择 foo 的 const& 版本。这些都是我理解的很好。

我很想知道一个反例,其中编译器在变量超出范围之前将其最后一次使用转换为右值引用会在生成的代码中引入一个严重的错误。如果编译器实现了这种“优化”,我想不出代码会中断。

如果当编译器自动使最后一次使用即将超出范围的变量成为右值引用时,没有代码中断,那么编译器为什么不将其作为优化来实现?

我的假设是一些代码会破坏编译器实现“优化”的位置,我想知道该代码是什么样的。

我在上面详述的代码是我认为可以从这样的优化中受益的代码示例。

函数参数的求值顺序,例如 operator+(foo(bob), foo(bob)) 是实现定义的。因此,代码如

return foo(bob) + foo(std::move(bob));

很危险,因为您使用的编译器可能会首先评估 + 运算符的右侧。这将导致字符串 bob 可能被移出,并使其处于有效但不确定的状态。随后, foo(bob) 将使用生成的修改后的字符串调用。

在另一种实现中,可能会首先评估非移动版本,并且代码的行为方式与非专家所期望的一样。

如果我们假设某些未来版本的 c++ 标准实现了允许编译器将变量的最后一次使用视为右值引用的优化,那么

return foo(bob) + foo(bob);

会毫无意外地工作(假设 foo 的适当实现,无论如何)。

这样的编译器,无论它对函数参数使用何种求值顺序,总是会在此上下文中将 bob 的第二次(也是最后一次)使用作为右值引用进行求值,无论那是左侧,还是运算符的右侧+。

【问题讨论】:

您通过声明foo(const string&amp;) 明确告诉编译器 进行移动优化。你还期待什么? 什么?我在哪里告诉编译器不要优化我作为示例提供的代码? 通过声明foo(const string&amp;)。这与优化无关。编译器不会猜测要优化什么。它调用最佳拟合函数来完成这项工作。你提供了一个。它调用它。您自己的决定。 你似乎完全错过了我的问题的重点?我很清楚在我的示例中将调用函数的 const& 版本。我在问为什么会这样。我的立场是 const& 版本不是 最合适的,因为编译器知道 bob 即将超出范围。我正在尝试查找示例代码,其中将 foo(bob) 替换为 foo(std::move(bob)) 的编译器会生成违反该语言某些方面的代码,以便我可以更好地理解该语言。 @jonesmz 这是一个有趣的问题,很高兴你提出这个问题。我为你不得不经历的麻烦让人们理解你的问题感到难过。 Michaël Roy 显然不明白,那些简单地说“标准不允许”的人没有抓住重点。甚至缺乏公认的答案,因为run_some_async_calculation_on_vector 无论如何都必须制作向量的副本,因为异步计算可能会在它返回之后继续(此时它的参数,即v 将无论如何都要被释放)。 【参考方案1】:

这是一段完全有效的现有代码,您的更改会破坏这些代码:

// launch a thread that does the calculation, moving v to the thread, and
// returns a future for the result
std::future<Foo> run_some_async_calculation_on_vector(std::pmr::vector<int> v); 

std::future<Foo> run_some_async_calculation() 
    char buffer[2000];
    std::pmr::monotonic_buffer_resource rsrc(buffer, 2000);
    std::pmr::vector<int> vec(&rsrc);
    // fill vec
    return run_some_async_calculation_on_vector(vec);

构造容器的移动总是传播它的分配器,但构造复制的不是必须的,polymorphic_allocator 是一个分配器,在容器复制构造上传播。相反,它总是恢复为默认内存资源。

此代码在复制时是安全的,因为run_some_async_calculation_on_vector 接收从默认内存资源分配的副本(希望在线程的整个生命周期中持续存在),但被移动完全破坏,因为那样它将保持rsrc 为内存资源,一旦run_some_async_calculation返回就会消失。

【讨论】:

标记为答案,因为它简洁地演示了代码会以一种微妙的方式中断,其中编译器从即将超出范围的变量中推断出右值引用。 一旦run_async_calculation 返回,它的参数v 无论如何都会超出范围。因此,对于要访问它的某些异步操作,它必须存储在异步操作期间将持续存在的位置。因此,要么 (A) 制作矢量的 副本 (在 run_async_calculation 的正文中),因此 OP 建议的优化不会破坏它或 ( B) 向量在run_async_code移动 导致它遇到您描述的相同问题-如果v 有临时内存资源,它在异步期间将不可用手术。因此,代码一开始就被破坏了。 @ricovox 不。 run_some_async_calculation_on_vector 的约定是“给我一个向量,我可以移动到线程中”。如果你违反合同并使用临时内存资源给它一个向量,那是你自己的错。【参考方案2】:

您的问题的答案是因为标准规定不允许这样做。编译器只能在 as if 规则下进行优化。 String 有一个大的构造函数,所以编译器不会做它需要的验证。

在这一点的基础上再进一步:编写在此优化下“中断”的代码所需要的只是让foo 的两个不同版本打印不同的东西。就是这样。编译器生成的程序打印的内容与标准规定的不同。那是一个编译器错误。请注意,RVO 属于此类别,因为该标准专门针对它。

问为什么标准没有这样说可能更有意义,例如为什么不扩展在函数末尾管理返回的规则,它被隐式地视为右值。答案很可能是因为定义正确行为很快变得复杂。如果最后一行是return foo(bob) + foo(bob),你会怎么做?等等。

【讨论】:

您能否澄清一下标准在哪些地方不允许这样做?当您说它只能在 as-if 规则下进行优化时,您是说它只能在该规则下进行任何优化,还是只能进行这种性质的优化? RVO 和 NRVO 在该语言的先前迭代中是由编译器完成的,尽管由于省略了构造函数/析构函数,它们显然存在行为差异。 你的第二段更有趣,因为它提出了使用什么逻辑的问题。在您的“return foo(bob) + foo(bob);”示例中我曾假设因为编译器知道它使用的评估顺序,例如函数调用参数等,编译器将简单地通过右值引用版本进行最后一次这样的调用,无论该调用碰巧是编译器订购事物的方式。 foo(bob) + foo(bob) 中,编译器将调用 foo() 的 const ref 版本。那是最适合参数的那个。编译器在这一点上非常具有确定性和严格性。它必须是。 仍然错过了我的问题,@MichaëlRoy。我想请您重新阅读它并就我实际询问的内容提供反馈。试图澄清。我不关心编译器现在做了什么,我关心的是为什么编译器没有利用潜在的优化。我怀疑我在我的问题中提到的优化实际上会破坏现实世界的代码,并试图找到一个代码示例,它会破坏编译器自动将变量的最后一次使用视为右值引用的地方. @jonesmz 我的意思是,标准定义了左值和右值,这就是左值。并且标准规定,由于bob 是一个左值,它必须 绑定到第二个foo 重载。这里的所有都是它的。现在,编译器可以在 as-if 规则下进行任何它想要的优化,但 as-if 显然是一个非常严格的准则。 RVO 和 NRVO 在 as-if 规则下,实际上 RVO 是由标准专门解决的。所以编译器可以做 RVO,因为标准是这么说的。【参考方案3】:

因为它将超出范围这一事实并不会使其在范围内时不是左值。所以 - 非常合理的 - 假设是程序员想要foo() 的第二个版本。并且该标准要求这种行为 AFAIK。

所以就写吧:

int main()

    std::string bob("  ");
    return foo(std::move(bob));

...但是,如果编译器可以内联foo(),则编译器可能会进一步优化代码,以获得与您的右值引用版本大致相同的效果。也许吧。

【讨论】:

【参考方案4】:

为什么编译器不会自动推断出变量即将超出范围,因此可以将其视为右值引用?

在函数被调用时,变量仍在作用域内。如果编译器根据函数调用后发生的情况改变了哪个函数更适合的逻辑,则将违反标准。

【讨论】:

为什么会违反标准? @jonesmz,该标准根据语句之前的所有内容定义程序的行为,而不是在语句之​​后。 @jonesmz:你是在问“为什么标准是这样的”? 不,我问的是标准中的哪些地方不允许这种性质的优化,或者寻找一个代码示例,该示例会在编译器上中断,该编译器会在一个值上推导出一个右值引用超出范围。 @jonesmz,我需要一些时间来查一下。

以上是关于编译器对超出范围的变量的右值引用进行推断的主要内容,如果未能解决你的问题,请参考以下文章

重新理解C11的右值引用

将此指针分配给对指针的右值引用

C11新特性右值引用&&

深入思考右值引用

对临时声明的右值引用

[转][c++11]我理解的右值引用移动语义和完美转发