【问题标题】:Tail-recursion not happening尾递归没有发生
【发布时间】:2015-05-06 21:14:29
【问题描述】:

我在 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 测试了这个函数,结果发现没有发生尾递归,这是我所期待的,因为returns 之后没有语句。

注意:我尝试使用 -O2 编译标志,但它不起作用

【问题讨论】:

  • funct的输入是什么?没有这个功能,很难说是哪里出了问题
  • @Matt 我认为funct 在这种情况下不相关。当我运行gdb 时,堆栈只调用了 Foo,因为调用了funct 然后它返回了。
  • 如果您在 if/else 语句中分配变量并且最后只调用一次 Foo 会发生什么?

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


【解决方案1】:

您的函数不是尾递归的,因为在递归调用之后仍有工作要做:销毁copy_bar(它有一个非平凡的析构函数),也可能还有baz (如果T 类型也有一个非平凡的析构函数)。

【讨论】:

    【解决方案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);
    }
    

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

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

    假设您想保持原来的 Foo 签名不变(而且,由于它有时会返回副本,因此没有理由相信您不想让签名保持不变),我们创建了一个辅助版本的Foo 称为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 的原始调用会支付构造和销毁本地对象的代价,而 Foo2 现在完全是尾递归的。

    【讨论】:

      【解决方案3】:

      我不认为@celschk 是正确的,而是@jxh 在他消灭的答案中,所以让我们重新审视它。

      存在需要销毁的局部变量这一事实通常不会影响尾递归。优化是把递归变成一个循环,这些变量可以在循环内部,并且在每次传递中都被销毁。

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

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

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

      【讨论】:

      • 注意:您可以手动进行此转换:在循环外创建一个空变量;只要条件为真,您就可以将循环内变量的内容与循环外的变量交换(这非常有效)并调整指针。如果您知道您将执行多次传递并因此需要多个副本,您还可以通过非常量 ref 获取值并提供一个外观,该外观将在转发到此之前复制用户的参数,然后您可以在内部进行爆炸(交换) 使用你得到的相同参数递归之前的参数值。
      • 注2:如果代码真的如您在上面显示的那样......好吧,您不需要参数的副本来调用size(),您可以完全删除该对象并制作您的生活(和编译器)更容易。
      • 我的回答的问题是,当我始终使用错误情况(在这种情况下,每次递归调用都传递相同的引用)进行测试时,尾递归仍然没有发生。唯一剩下的是copy_bar 的析构函数代码。
      • ...以及与递归函数返回值关联的构造函数/析构函数。
      猜你喜欢
      • 1970-01-01
      • 2021-05-30
      • 2016-03-21
      • 1970-01-01
      • 2018-03-17
      • 2012-05-16
      • 1970-01-01
      • 2017-11-11
      • 1970-01-01
      相关资源
      最近更新 更多