尾递归没有发生

时间:2015-05-06 21:14:29

标签: c++ recursion g++ tail-recursion

我在C ++项目中使用g++ (Ubuntu 4.8.2-19ubuntu1) 4.8.2。我写了一个函数来做到这一点:

template<typename T, T (*funct)(int) >
multiset<T> Foo(const multiset<T>& bar, int iterations) {
    if (iterations == 0) return bar; 
    multiset<T> copy_bar(bar); 

    T baz = funct(copy_bar.size());

    if (baz.attr > 0)
        return Foo<T,funct>(copy_bar, 100);
    else 
        return Foo<T,funct>(bar, iterations - 1);    
}

我得到了bad_alloc() exception所以我用gdb测试了这个函数,结果发现没有发生尾递归,这是我所期待的,因为在{{之后没有语句1}} S上。

注意:我尝试使用-O2编译标记,但它没有工作

3 个答案:

答案 0 :(得分:3)

你的函数是尾递归的,因为在递归调用之后还有工作要做:copy_bar的破坏(有一个非平凡的析构函数),也可能是{ {1}}(如果类型baz也有一个非平凡的析构函数)。

答案 1 :(得分:2)

@celtschk's answer所示,非平凡的析构函数阻止编译器将调用视为真正的尾递归。即使你的功能减少到这个:

template<typename T, T (*funct)(int) >
multiset<T> Foo(const multiset<T>& bar, int iterations) {
    if (iterations == 0) return bar;
    return Foo<T,funct>(bar, iterations - 1);
}

它仍然不是尾递归的,因为对递归函数调用的结果的非平凡构造函数和析构函数的隐式调用。

但请注意,上述函数确实变为尾递归,只有相对较小的变化:

template<typename T, T (*funct)(int) >
const multiset<T>& Foo(const multiset<T>& bar, int iterations) {
    if (iterations == 0) return bar;
    return Foo<T,funct>(bar, iterations - 1);
}

瞧!该函数现在编译成循环。我们如何使用您的原始代码实现这一目标?

不幸的是,它有点棘手,因为您的代码有时会返回副本,有时会返回原始参数。我们必须正确处理这两种情况。下面给出的解决方案是他的评论中提到的David概念的变体。

假设您希望保持原始Foo签名相同(并且,因为它有时会返回副本,因此没有理由相信您不想让签名保持不变),我们创建了一个名为Foo的{​​{1}}的辅助版本,它返回对结果对象的引用,该结果对象是原始Foo2参数,或bar中的一个本地参数。此外,Foo为副本创建占位符对象,创建辅助副本(用于切换),以及用于保存Foo调用结果的对象:

funct()

由于template<typename T, T (*funct)(int) > multiset<T> Foo(const multiset<T>& bar, int iterations) { multiset<T> copy_bar; multiset<T> copy_alt; T baz; return Foo2<T, funct>(bar, iterations, copy_bar, copy_alt, baz); } 总是会返回一个引用,这会删除递归函数结果导致隐式构造和破坏的任何问题。

每次迭代时,如果要将副本用作下一个Foo2,我们将副本传入,但是为递归调用切换副本和备用占位符的顺序,以便替换实际上是用于在下一次迭代中保存副本。如果下一次迭代按原样重用bar,则参数的顺序不会改变,bar计数器只会递减。

iterations

请注意,只有对template<typename T, T (*funct)(int) > const multiset<T>& Foo2( const multiset<T>& bar, int iterations, multiset<T>& copy_bar, multiset<T>& copy_alt, T& baz) { if (iterations == 0) return bar; copy_bar = bar; baz = funct(copy_bar.size()); if (baz.attr > 0) { return Foo2<T, funct>(copy_bar, 100, copy_alt, copy_bar, baz); } else { return Foo2<T, funct>(bar, --iterations, copy_bar, copy_alt, baz); } } 的原始调用才能支付构造和破坏本地对象的惩罚,Foo现在完全是尾递归的。

答案 2 :(得分:1)

我不认为@celschk是对的,而是@jxh在他消失的答案中,所以让我们重温一下。

需要销毁局部变量这一事实通常不会影响尾递归。优化是将递归转换为循环,这些变量可以在循环内部并在每次传递中被销毁。

我认为问题源于这样一个事实,即参数是一个引用,并且根据某些条件,每次遍历循环都必须引用函数外部的对象或此函数内的本地副本。如果您尝试手动将递归展开到循环中,您会发现很难弄清楚循环应该是什么样的。

要将递归转换为循环,您必须在循环外部创建一个额外的变量来保存multimap的值,将引用转换为指针并将指针更新为一个或另一个对象取决于跳回到循环开始之前的条件。 baz变量不能用于此(即它不能被拉到循环之外),因为每个传递都会复制,我想象一些其他变换,你没有在上面的代码中显示,所以你真的需要创建一个额外的变量。编译器无法为您创建新变量。

此时,我必须承认是的,这里的问题是,在递归完成后,其中一个分支copy_var需要被销毁(作为对它的引用)传递下去,所以@celtschk不是100%错误的......但是当他指向baz作为打破尾递归的另一个潜在原因时他就是这样。