1 Boost.Stacktrace
当程序发生错误的时候,能提供的信息越多,对错误的定位就越有利。C#、Pyrhon、Java 等编程语言都提供调用栈踪迹回溯的功能,在错误发生的时候,除了报告错误发生的位置,还能输出函数调用栈信息。但是 C++ 无论语言特性还是库,都不支持调用栈的回溯,当错误发生的时候,通过传统的宏或 C++ 20 的 std::source_location,也只能得到错误发生时的位置信息,当这个位置存在于多个函数调用链上的时候,就无法得知错误发生的源头,对错误的定位非常不利。
有很多第三方的调试支持库提供栈回溯功能,比如 Boost.Stacktrace 库,这是使用 boost 库输出函数调用栈的例子:
#include <boost/stacktrace.hpp>
using namespace bst = boost::stacktrace;
void Test{
std::cout << bst::stacktrace();
}
int main() {
Test();
}
作为 C++ 标准库的技术储备库,许多支持者希望将 Boost.Stacktrace 转正。不过也有一些反对的声音,主要是两点,其一是性能,大家希望 Boost.Stacktrace 能够快一点,但是 Boost.Stacktrace 的实现是在调用 stacktrace() 的时候就将整个调用栈全部解码成可读的字符串,在很多人看来没必要这么做,在需要输出可读信息的时候再做这个解码才是合理的。另一个问题是存储信息量的问题。 Boost.Stacktrace 的实现不依赖任何标准库的组件,所以也没有使用分配器,它使用一个固定大小的存储区存储栈的信息,这对导致调用栈比较深的时候,调用栈低端的一些重要信息无法存下来。
stacktrace_22">2 std::stacktrace
2.1 基础结构
P0881R7 提案建议 C++ 支持栈踪迹库,其设计就是基于 Boost.Stacktrace 库,但是很好地解决了上面提到的两个问题。最终提案被 C++ 接纳,成为 C++ 23 标准的一部分。C++ 栈踪迹库的基础结构是 std::basic_stacktrace 类模板,stacktrace 是使用标准库的默认内存分配器的一个类型别名:
template <class _Alloc>
class basic_stacktrace { ... }
using stacktrace = basic_stacktrace<allocator<stacktrace_entry>>;
默认的 allocator 是在堆上分配调用栈信息,对安全性要求很高的场合,用户可以提供定制的 allocator,在栈上分配空间存储调用栈信息。类似前面 Boost 库的例子,直接输出当前调用栈信息的方法是:
std::cout << std::stacktrace::current();
std::basic_stacktrace 类的静态成员函数 current() 返回一个包含完整调用栈信息的 std::basic_stacktrace<> 对象实例,此时其中的信息还依赖于操作系统的内部接口,可读信息的解码是在调用重载的流输出操作符时才进行的,这就是这个库在设计上所提到的 lazy 模式。
stacktrace_entry_43">2.2 std::stacktrace_entry
std::stacktrace_entry 代表的是每层(Frame)调用栈的信息,我们可以通过遍历 basic_stacktrace 对象获取每一层的信息,也可以通过 basic_stacktrace 的 at() 成员函数或下标运算符重载获得对应层的信息:
std::stacktrace stack = std::stacktrace::current();
const std::stacktrace_entry& ste = stack[0];
std::cout << ste.description() << std::endl;
std::cout << ste.source_file() << std::endl;
std::cout << ste.source_line() << std::endl;
stacktrace_56">2.3 遍历 std::stacktrace
std::stacktrace 提供了 begin() 和 end(),可以通过它们返回的迭代器遍历调用栈信息:
std::stacktrace stack = std::stacktrace::current();
for (auto it = stack.begin(); it != stack.end(); ++it)
std::cout << *it << std::endl;
std::stacktrace 还提供了 size() 函数和下标运算符重载,可以通过索引遍历调用栈信息:
auto stack = std::stacktrace::current();
for(std::size_t i = 0; i < stack.size(); i++)
std::cout << stack[i] << std::endl;
当然,最“甜”的方法就是直接用 for 循环遍历:
auto stack = std::stacktrace::current();
for(auto&& entry : stack)
std::cout << entry << std::endl;
2.4 字符串与输出
前面的例子代码中,我们可以用流输运算符直接输出 std::stacktrace 和 std::stacktrace_entry,是因为这两个类都提供了流输出运算符的重载。当然,这两个类也提供了 to_string() 函数的重载,可以利用 to_string() 直接得到调用栈信息的字符串,比如:
auto stack = std::stacktrace::current();
const std::stacktrace_entry& ste = stack[0];
//../../../soucr.cpp(172): NothingFind + 0x168
std::cout << std::to_string(ste) << std::endl;
3 C++ 26 的展望
当异常发生的时候,在异常捕捉的位置能获得异常发生时的函数调用栈信息吗?试着运行一下下面的代码?
void foo() {
throw std::runtime_error("foo failed");
}
try {
foo();
}
catch (const std::exception& e) {
std::cerr << std::stacktrace::current() << std::endl;
}
结果得到的是异常处理位置的调用栈,不是我们希望的 foo() 函数中扔出异常的位置。cpptrace 库[资料 5] 有一个非常实用的功能,它提供了一个 from_current_exception() 函数,用于在异常捕捉位置获取异常发生时的函数调用栈信息。与其对 cpptrace 库流口水,不如赶紧给 C++ 上提案,P2370R0 [资料 6] 提出 C++ 也要提供一个 from_current_exception() 函数,用于在异常处理位置捕获函数调用栈信息。P2370 系列提案最大的问题就是会给异常处理部分增加额外的开销,这显著违反了一个重要的 C++ 原则,即“当你不需要的时候,你不必为此付出代价”。
P2490 [资料 3] 系列提案提出的一种零开销方案,就是增加一个 [[with_stacktrace]] 属性说明符,用于指示编译器在该位置保留异常发生时的函数调用栈信息,如下例子所示:
try {
...
} catch ([[with_stacktrace]] std::exception& e) {
std::cout << std::stacktrace::from_current_exception() << std::endl;
}
如果用户没有在 catch 语句中使用这个说明符,则编译器不会保留异常发生时的函数调用栈信息,也就没有额外的开销。不过 P2490 目提案在 2022 年 6 月提出 P2490R3 版本之后就没有什么动作了,看起来进入 C++ 26 的希望有点渺茫。
4 总结
stacktrace__132">4.1 C++ 23 stacktrace 库的特点
C++ 的 stacktrace 库结构简单,使用也简单,主要特点是:
- 所有的 stacktrace_entry 都是延迟绑定,只有调用 to_string() 或 operator << 时才对栈帧信息解码;
- 栈帧信息采用动态存储,最重要的底部栈帧信息也会存储
- 简单
简单如果也算的话,那就是三个特点,这里再说说第二条。前面其实也提过,因为 C++ 的 stacktrace 库一般情况下使用标准库的默认分配器,也就是动态分配在堆上。使用堆内存可能的问题就是性能问题,对于一些性能优先的函数调用路径上使用 C++ 的 stacktrace 库要非常慎重。从性能方面考虑,可以考虑用 tcmalloc 之类的第三方库提供的分配器代替标准库的默认分配器。从安全方面考虑,也可以构造在栈上分配空间的特殊分配器代替默认分配器,这个前面也提到过了。
4.2 其他第三方库
如果你的编译器还不支持 stacktrace 库,你可以使用 Boost.Stacktrace 库,当然,还有很多优秀的库可以选择,比如 backward-cpp,cpptrace 等等。
参考资料
[1] https://github.com/boostorg/stacktrace
[2] P0881R7: A Proposal to add stacktrace library
[3] P2490R3: Zero-overhead exception stacktraces (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2490r3.html)
[4] https://github.com/bombela/backward-cpp
[5] https://github.com/jeremy-rifkin/cpptrace
[6] P2370R0: Stacktrace from exception (https://www9.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2370r0.html)
[7] https://github.com/jeremy-rifkin/cpptrace?tab=readme-ov-file#traces-from-all-exceptions
关注作者的算法专栏
https://blog.csdn.net/orbit/category_10400723.html
关注作者的出版物《算法的乐趣(第二版)》
https://www.ituring.com.cn/book/3180