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

Lou*_*man 25 c++ compiler-construction recursion 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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

Mik*_*son 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
};
Run Code Online (Sandbox Code Playgroud)

现在,一个天真的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.
Run Code Online (Sandbox Code Playgroud)

但是一个聪明的程序员仍然可以看到这不起作用(除非向量析构函数是内联的,在这种情况下很可能),并且可以很容易地纠正这种情况:

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
};
Run Code Online (Sandbox Code Playgroud)

而且我很确定你可以证明基本上没有这种技巧不能用于确保可以应用TCO的情况.因此,RAII只是让人更难以确定是否可以应用TCO.但我认为编程能够设计具有TCO功能的递归调用的程序员也足够明智地看到那些需要在尾调用之前强制发生的"隐藏"析构函数调用.

添加注意:以这种方式查看,析构函数隐藏了一些自动清理代码.如果你需要清理代码(即非平凡的析构函数),无论你是否使用RAII,你都需要它(例如,C风格的数组或其他).然后,如果你想要TCO是可能的,那么必须有可能在进行尾调用之前进行清理(有或没有RAII),并且有可能,然后可能强制RAII对象被销毁在尾调用之前(例如,将它们放在额外​​的范围内).

  • @supercat:标准指定了可观察的行为(即,行为"好像"以相反的顺序被破坏),这是一个薛定谔猫的问题,只要你添加代码使得执行顺序可观察,标准保证顺序,但是当您删除该代码时,您无法确定您无法观察到的事物的顺序. (2认同)

Cas*_*eri 5

如果编译器执行TCO,则调用析构函数的顺序相对于不执行TCO的时间而改变.

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