【问题标题】:How do I capture the results of a recursive function at compile-time?如何在编译时捕获递归函数的结果?
【发布时间】:2015-10-21 06:10:35
【问题描述】:
#include <iostream>

template <typename T>
struct node {
    T value;
    node const* prev;

    constexpr node(const T& value, node const* prev = nullptr)
        : value{value}, prev{prev} {}

    constexpr node push_front(const T& value) const {
        return node(value, this);
    }
};

struct Something {
    node<int> n;

    constexpr Something(const int i) : n{node<int>(i)} {}

    constexpr Something(const node<int>& n) : n{n} {}
};

constexpr void print(const Something& s) {
    bool first = true;
    for (const node<int>* i = &s.n; i != nullptr; i = i->prev) {
        if (first) {
            first = false;
        } else {
            std::cout << ", ";
        }
        std::cout << i->value;
    }
}

constexpr Something recursive_case(Something& s, const unsigned int i) {
    Something result(s.n.push_front(i % 10));
    auto j = i / 10;
    return j != 0 ? recursive_case(result, j) : result;
}

constexpr Something base_case(const unsigned int i) {
    Something result(i % 10);
    auto j = i / 10;
    return j != 0 ? recursive_case(result, j) : result;
}

int main() { print(base_case(21)); }

我有一个像上面所示的递归函数(base_caserecursive_case)。我从这个链接中得到了node 对象的想法:https://gist.github.com/dabrahams/1457531#file-constexpr_demo-cpp-L66,然后我对其进行了修改以满足我的需要。我上面的问题是我遇到了分段错误。

谢谢。

编辑:

  1. 很抱歉没有早点尝试调试器。这是输出:

        $ lldb ./ww                                                                                              ~/scratch/ww
    (lldb) target create "./ww"
    Current executable set to './ww' (x86_64).
    (lldb) run
    Process 32909 launched: './ww' (x86_64)
    Process 32909 stopped
    * thread #1: tid = 0x4d4e8e, 0x000000010000109b ww`print(s=0x00007fff5fbfec80) + 91 at ww.cpp:32, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
        frame #0: 0x000000010000109b ww`print(s=0x00007fff5fbfec80) + 91 at ww.cpp:32
       29           } else {
       30               std::cout << ", ";
       31           }
    -> 32           std::cout << i->value;
       33       }
       34   }
       35
    (lldb)
    

    我会尝试使用new 或智能指针,但根据我的阅读,它们不能在constexpr 函数中使用。

  2. 我尝试同时使用new 和智能指针。

    • 对于new,我收到此错误:

          ww.cpp:19:15: error: constexpr constructor never produces a constant expression [-Winvalid-constexpr]
              constexpr Something(const int i) : n{new node<int>(i)} {}
                        ^
          ww.cpp:19:42: note: subexpression not valid in a constant expression
              constexpr Something(const int i) : n{new node<int>(i)} {}
                                                   ^
      
    • 对于unique_ptr,我收到此错误:

          ww.cpp:26:11: note: non-constexpr constructor 'unique_ptr' cannot be used in a constant expression
          : n{std::unique_ptr<node<int>, deleter<node<int>>>(new node<int>(i))} {}
      
  3. 我研究了一些,我认为可以使用 C++ 模板解决这个问题。我只需要一种方法来捕获递归的中间结果,例如某种编译时列表,然后颠倒顺序。

【问题讨论】:

  • 如果您遇到崩溃,您应该在调试器中运行以捕获崩溃,并查看它在您的代码中发生的位置。至少,请编辑您的问题,在代码中包含崩溃的位置,以及所涉及的不同变量的值。
  • 删除所有的constexpr,你应该可以使用调试器单步执行,看看哪里出了问题。递归看起来是有限的,所以我的钱在i != nullptr上。跨度>
  • 我认为这个算法不能正确地用于构建 constexp 函数。有关详细信息,请参阅我编辑的答案。

标签: templates recursion c++14 template-meta-programming constexpr


【解决方案1】:

这是一个有趣的问题,但正如 Joachim Pileborg 在评论中所说,在调试器中执行程序会告诉你崩溃的原因。

首先是诊断:

  • 点击print 函数时一切正常:s 包含一个带有前一个节点的节点,仅此而已 (s.prev-&gt;prev == nullptr)
  • std::cout &lt;&lt; i-&gt;value; 之后它坏了:s.prev-&gt;prev != nullptr 当指令不应该改变它时!

因为它在调用函数时中断,所以闻起来像是有一个悬空的指针或引用...

现在解释一下:

在您的递归调用中,所有内容(Somethingnodes 都作为局部变量分配并作为参考传递给递归调用。当调用向下时,一切都很好,所有内容都在调用函数中分配。但是当你返回时,只有Something 和它的node 是正确的:所有后续节点都被分配为现在结束函数中的局部变量并且(调用result 返回值)result.prev 是一个悬空指针。

使用悬空指针是未定义的行为,可以使用 SIGSEGV。

TL/DR:您将链表的节点分配为递归调用中的局部变量,并以悬空指针结束,因此未定义的行为可能会立即导致崩溃。

注意:未定义的行为可能在您的所有测试中都起作用,并在以后的生产中中断

可能的解决方法:由于类不能包含自身的实例,因此不能在链表中使用按值返回,而必须使用动态分配。所以你必须实现显式析构函数或使用智能指针。

以上主要是对崩溃的解释。它可以通过使用智能指针来修复,但不能再使用constexpr。如果 struct node 被这样修改:

template <typename T>
struct node {
    T value;
    std::shared_ptr<node const> prev;

    node(const T& value, node const* prev = nullptr)
        : value{value} {
    this->prev = (prev == nullptr)
        ? std::shared_ptr<struct node const>(nullptr)
        : std::make_shared<struct node const>(*prev);
    }

    node push_front(const T& value) const {
        return node(value, this);
    }
};

它可以通过复制安全地返回,因为shared_ptr 确保您永远不会得到一个悬空指针。但是构造函数确实不能再是constexpr,所以只剩下print了……

但这是有道理的。当recursive_case 反复构建一个链表时,我无法想象有一种方法可以使它成为 constexpr。可以通过首先分配nodes 的数组(因为这里每个数字需要node)然后链接那些现有 节点。但是如果它们被分配为递归函数中的局部变量,您将无法避免悬空问题,并且如果它们是动态分配的,则不能是constexpr

【讨论】:

    【解决方案2】:

    添加

    constexpr node(const node& i_rhs)
        : value(i_rhs.value)
        , prev(i_rhs.prev == nullptr ? nullptr : new node(*i_rhs.prev))
    {}
    
    constexpr node(const node&& i_rhs)
        : value(i_rhs.value)
        , prev(i_rhs.prev)
    {}
    

    为我工作。

    据我了解,整个场景如下:

    1. 程序进入 base_case 并在堆栈上创建第一个 Something。
    2. 程序进入 recursive_case 并在堆栈上创建第二个 Something,将其与第一个 something 链接。
    3. 程序离开 recursive_case 并且第二个Something被复制并返回。
    4. 程序离开 base_case 返回第二个 Something 并且第一个 Something 没有被复制到任何地方 - 它仍然存在于堆栈中(在其中未使用的部分)。
    5. 程序进入打印 - 该指令使用堆栈内存,首先Something(位于堆栈的未使用部分)被垃圾值填充。
    6. 程序尝试首先访问Something并出现段错误。

    此修复程序通过执行对象的深度复制解决了此问题。但是这个修复会导致内存泄漏:在构造函数中分配的内存永远不会被释放。这可以通过使用智能指针或显式实现析构函数来解决。

    【讨论】:

    • 暴力修复。 OP 所要做的就是首先不将局部变量加载到指针中,所有问题都消失了。
    • 差不多。他们需要delete 节点。
    • @user4581301,好吧,无论如何,如果作者想要复制或移动节点对象,他应该正确处理所有权
    • 完全同意这一点。但是最初应该使用 new 创建 Node 而不是在复制构造函数中完成,因为在复制构造函数中没有简单的方法来判断指针是否指向返回的临时或动态 newed 在前一次通过副本时构造函数。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-27
    • 1970-01-01
    • 1970-01-01
    • 2011-07-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多