带有对象的尾递归

Tail-recursion with objects

本文关键字:尾递归 对象      更新时间:2023-10-16

我有一个递归函数,我想让它成为尾递归函数。我的实际问题更复杂且依赖于上下文。但是我想解决的问题通过这个简单的程序演示:

#include <iostream>
struct obj
{
    int n;
    operator int&() { return n; }
};
int tail(obj n)
{
    return tail(obj{ n + 1 > 1000 ? n - 1000 : n + 1 });
}
int main()
{
    tail(obj{ 1 });
}

这似乎是很自然的尾递归。但事实并非如此,因为每次都必须调用obj n的析构函数。至少 MSVC13 (编辑:)MSVC15 不会对此进行优化。如果我用 int 替换 obj 并相应地更改调用,它会按预期变为尾递归。

我的实际问题是:除了用int替换obj之外,有没有一种简单的方法可以使这种尾递归?我的目标是性能优势,因此使用堆分配的内存和new很可能没有帮助。

简短回答:否。

更长的答案:你可能会找到一种方法来实现这一目标,但肯定不容易。由于标准不需要尾部调用优化,因此您永远无法确定对程序的一些微小更改是否会导致编译器无法优化代码。

更糟糕的是,考虑一下当您需要调试程序时会发生什么。编译器几乎肯定不会使用调试器标志优化高级尾部调用,这意味着您的程序只能在发布模式下正常工作。这将使程序更难维护。

尾递归的替代方法只需编写一个循环。它总是可以做到的,而且可能要复杂得多。它也不使用堆,因此开销会小得多。

由于您使用临时对象,因此我假设您在递归调用后不需要该对象。

一个

相当黑客的解决方案是分配一个对象,传递一个指向它的指针,并在进行递归调用之前重新分配它,您将新构建的对象传递给递归调用。

struct obj
{
    int n;
    operator int&() { return n; }
};
int tail_impl(obj*& n)
{
    int n1 = *n + 1 > 1000 ? *n - 1000 : *n + 1;
    delete n;
    n = new obj{n1};
    return tail_impl(n);
}
int tail(obj n)
{
  obj *n1 = new obj{n};
  auto ret = tail_impl(n1);
  delete n1;
  return ret;
}
int main()
{
    tail(obj{ 1 });
}

我显然省略了一些关键的异常安全细节。然而,GCC能够将tail_impl变成一个循环,因为它确实是尾递归。