如何在编译时捕获递归函数的结果?

时间:2015-10-21 06:10:36

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

#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 ++模板可以解决这个问题。我只需要一种方法来捕获递归的中间结果,例如某种编译时列表然后颠倒顺序。

2 个答案:

答案 0 :(得分:3)

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

首先是诊断:

  • 在点击print功能时一切正常:s包含一个包含前一个节点的节点,并且全部(s.prev->prev == nullptr
  • std::cout << i->value;它被破坏之后:s.prev->prev != nullptr当指令不应该改变它时!

当它在调用函数时断开,它闻起来像是一个悬挂的指针或引用...

现在解释:

在你的递归调用中,所有内容(Somethingnode都被分配为局部变量并作为引用传递给递归调用。当调用时,一切都很好,所有内容都分配在一个调用函数。但是当你返回时,只有Something及其node被更正:所有后续节点都被分配为现在结束函数中的局部变量,并且(调用result返回值) result.prev是一个悬垂的指针。

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

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

警告:未定义的行为可以在所有测试中起作用,并在生产后期中断

可能的修复:由于类不能包含自身的实例,因此不能在链表中使用return by value,并且必须使用动态分配。因此,您必须实现显式析构函数或使用智能指针。

以上主要是对坠机事件的解释。可以使用智能指针修复它,但不能再使用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。首先可以分配一个node的数组(因为这里每个数字需要node),然后链接那些现有的节点。但如果它们在递归函数中被分配为局部变量,则无法避免悬空问题,如果它们是动态分配的,则不能是constexpr

答案 1 :(得分:1)

添加

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并首先创建堆栈上的东西。
  2. 程序输入recursive_case并在堆栈上创建第二个Something,将其与第一个东西链接。
  3. 程序离开recursive_case,然后复制并返回Something。
  4. 程序让base_case返回第二个Something和第一个Something不会被复制到任何地方 - 它仍然存在于堆栈中(在其未使用的部分)。
  5. 程序输入print - 此指令使用堆栈内存,首先Something(它位于堆栈的未使用部分)由垃圾值填充。
  6. 程序尝试首先访问Something并获取seg错误。
  7. 此修复程序通过执行对象的深层复制来解决此问题。但是此修复导致内存泄漏:构造函数中分配的内存永远不会被释放。这可以通过使用智能指针或显式实现析构函数来解决。