为什么返回类型存在非平凡析构函数会阻止尾调用优化?

3

目前,在C++编译器中,尾调用优化的规则之一是返回类型必须是可平凡销毁的。(基于对GCC、Clang trunk行为的分析。MSVC在处理任何非平凡类型时都有问题)。

这个要求还有必要吗?随着C++17强制使用返回值优化,似乎即使返回类型是非平凡的,函数仍然可以使用尾调用优化。那么这里的问题是什么,阻止编译器实现这一点呢?

@编辑,代码示例:

#include <string>

bool h();

std::string g() {
    std::string s1 = "a", s2 = "b";
    if (h()) return s1;
    else return s2;
}

std::string f() {
    return g();  // <= here I'd expect call-tail optimization due to RVO, since it is prvalue
}

https://godbolt.org/z/YYfMr6xdd

如果我正确理解汇编语言,应该可以用跳转指令来替换f()函数。


如果我理解正确,f 可以被 g 替换(内联)(我猜这不是必须的)。而且由于 g 有多个返回路径,所以它没有 NRVO(或 RVO)。 - alfC
1
gcc和clang都保存并恢复$rdi->$rax。也许它们不认为g正确设置了$rax? - dyp
2
这与trivially-destructible并不直接相关,而是与ABI是否允许通过寄存器传递有关。如果类型是trivially destructible但足够大,则尾调用也不会被优化:https://godbolt.org/z/TbrM74j7b - dyp
@dyp,是的,这是我第一次听到与(N)RVO相关的琐碎销毁。我认为不是琐碎就不能防止(N)RVO。隐含的想法是省略的销毁如果在第一次省略构造时没有副作用或者不重要。 - alfC
我几乎可以确定,将这些值传递到寄存器中取决于类型的平凡可析构性。Clang的[[clang::musttail]]属性会返回一个错误,指示该类型必须是平凡可析构的,并且在许多平凡可重定位/比特复制可移植性提案中使用相同的措辞。经过一些研究后,我听到了Herb Sutter / Niall Douglas演讲中的同样的事情,如果我理解他们正确的话,这种小值ABI效率低下是由于非平凡的析构函数引起的。 - Kaznov
1个回答

0

仅当您返回一个临时对象(更准确地说是prvalue - 请参见cppreference.com上的这里)时,才需要使用返回值优化,例如在以下情况下:

std::vector<int> foo() {
  return std::vector<int>{1,2};
}

如果你正在返回一个本地对象,这就是命名返回值优化(也称“拷贝省略”),这不是强制性的(但大多数情况下还是会这么做),例如在这种情况下:

std::vector<int> foo() {
  std::vector<int> myVec{1,2};
  return myVec;
}

在这种情况下,编译器允许首先在foo()的框架内构造myVec,然后通过复制返回并销毁。显然,大多数编译器不会这样做。但是,当foo()结束时,销毁是可能发生的。

我猜这就是要求平凡析构函数进行尾调用优化的原因。如果foo()进行了尾调用优化,则标准必须考虑创建/复制返回/销毁生命周期,并因此有一种(平凡地)销毁返回对象的方式。


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