尾部呼叫优化和RAII可以共存吗?

时间:2013-07-22 16:37:06

标签: c++ recursion compiler-construction tail-recursion raii

我想不出真正的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);
}

“构建......”将写入999次,然后“毁灭......”再写999次。最终,在展开之前将自动分配999个test_object个实例。但假设一个实现具有TCO,那么将存在1000个堆栈帧还是仅存在1?

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

2 个答案:

答案 0 :(得分:15)

从面值来看,看起来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对象被销毁在尾调用之前(例如,将它们放在额外​​的范围内)。

答案 1 :(得分:5)

如果编译器执行TCO,则调用析构函数的顺序会在不执行TCO时发生更改。

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