30

我想不出真正的 RAII 语言在规范中也有尾调用优化,但我知道许多 C++ 实现可以将其作为特定于实现的优化来实现。

这对那些这样做的实现提出了一个问题:假设析构函数是在自动变量作用域的末尾而不是由单独的垃圾收集例程调用的,它是否违反了 TCO 的约束,即递归调用必须是最后一条指令函数结束?

例如:-

#include <iostream>

class test_object {
public:
    test_object() { std::cout << "Constructing...\n"; }
    ~test_object() { std::cout << "Destructing...\n"; }
};

void test_function(int count);

int main()
{
    test_function(999);
}

void test_function(int count)
{
    if (!count) return;
    test_object obj;
    test_function(count - 1);
}

“Constructing...”将被写 999 次,然后“Destructing...”又被写 999 次。test_object最终,在展开之前将自动分配999 个实例。但是假设一个实现有 TCO,会存在 1000 个堆栈帧还是只有 1 个?

递归调用后的析构函数是否与事实上的 TCO 实现要求相冲突?

4

2 回答 2

17

从表面上看,RAII 似乎对 TCO 有效。但是,请记住,编译器可以通过多种方式“摆脱它”,可以这么说。

第一个也是最明显的情况是如果析构函数是微不足道的,这意味着它是默认析构函数(编译器生成)并且所有子对象也有微不足道的析构函数,那么析构函数实际上不存在(总是优化掉)。在这种情况下,可以照常执行 TCO。

然后,可以内联析构函数(它的代码被直接放入函数中,而不是像函数一样被调用)。在这种情况下,归结为在 return 语句之后有一些“清理”代码。如果编译器可以确定最终结果相同(“as-if”规则),则允许编译器重新排序操作,并且如果重新排序导致更好的代码,它将这样做(通常),我会假设 TCO 是大多数编译器应用的考虑因素之一(即,如果它可以重新排序以使代码适合 TCO,那么它会这样做)。

而对于其余的情况,编译器不能“足够聪明”地自行完成,那么它就变成了程序员的责任。这种自动析构函数调用的存在确实使程序员在尾部调用之后更难看到抑制 TCO 的清理代码,但就程序员进行作为 TCO 的候选者。例如:

void nonRAII_recursion(int a) {
  int* arr = new int[a];
  // do some stuff with array "arr"
  delete[] arr;
  nonRAII_recursion(--a);  // tail-call
};

现在,一个简单的RAII_recursion实现可能是:

void RAII_recursion(int a) {
  std::vector<int> arr(a);
  // do some stuff with vector "arr"
  RAII_recursion(--a);  // tail-call
};  // arr gets destroyed here, not good for TCO.

但是聪明的程序员仍然可以看到这是行不通的(除非向量析构函数是内联的,在这种情况下很可能),并且可以轻松地纠正这种情况:

void RAII_recursion(int a) {
  {
    std::vector<int> arr(a);
    // do some stuff with vector "arr"
  }; // arr gets destroyed here
  RAII_recursion(--a);  // tail-call
};

而且我很确定您可以证明基本上没有不能使用这种技巧来确保可以应用 TCO 的情况。因此,RAII 只是让查看 TCO 是否可以应用变得更加困难。但我认为足够聪明地设计具有 TCO 能力的递归调用的程序员也足够聪明地看到那些需要在尾调用之前强制发生的“隐藏”析构函数调用。

补充说明:这样看,析构函数隐藏了一些自动清理代码。如果您需要清理代码(即,非平凡的析构函数),无论您是否使用 RAII(例如,C 样式数组或其他),都将需要它。然后,如果您希望 TCO 成为可能,则必须可以在进行尾调用之前进行清理(有或没有 RAII),并且有可能,然后有可能强制销毁 RAII 对象在尾调用之前(例如,通过将它们放在额外​​的范围内)。

于 2013-07-22T17:19:27.390 回答
5

如果编译器执行 TCO,那么调用析构函数的顺序会根据它不执行 TCO 的时间而改变。

如果编译器可以证明这种重新排序无关紧要(例如,如果析构函数是微不足道的),那么根据as-if规则,它可以执行 TCO。但是,在您的示例中,编译器无法证明这一点,也不会执行 TCO。

于 2013-07-22T17:01:05.573 回答