我正在写一个我想要移植的库.因此,它不应该依赖于glibc或Microsoft扩展或标准中没有的任何其他内容.我有一个很好的从std :: exception派生的类层次结构,我用它来处理逻辑和输入中的错误.知道在特定文件和行号处抛出特定类型的异常是有用的,但是知道执行的执行方式可能会更有价值,所以我一直在寻找获取堆栈跟踪的方法.
我知道,使用execinfo.h功能(见的是glibc建造时这个数据是可用的质疑76822),并通过微软的C++实现的StackWalk接口(见问题126450),但我非常希望避免任何的不便携.
我正在考虑以这种形式自己实现这个功能:
class myException : public std::exception { public: ... void AddCall( std::string s ) { m_vCallStack.push_back( s ); } std::string ToStr() const { std::string l_sRet = ""; ... l_sRet += "Call stack:\n"; for( int i = 0; i < m_vCallStack.size(); i++ ) l_sRet += " " + m_vCallStack[i] + "\n"; ... return l_sRet; } private: ... std::vector< std::string > m_vCallStack; }; ret_type some_function( param_1, param_2, param_3 ) { try { ... } catch( myException e ) { e.AddCall( "some_function( " + param_1 + ", " + param_2 + ", " + param_3 + " )" ); throw e; } } int main( int argc, char * argv[] ) { try { ... } catch ( myException e ) { std::cerr << "Caught exception: \n" << e.ToStr(); return 1; } return 0; }
这是一个糟糕的主意吗?这意味着在每个函数中添加try/catch块会有很多工作,但我可以忍受.当异常的原因是内存损坏或内存不足时,它将无法工作,但在那时你无论如何都要搞砸了.如果堆栈中的某些函数没有捕获异常,将自己添加到列表中并重新抛出,它可能会提供误导性信息,但我至少可以保证我的所有库函数都这样做.与"真正的"堆栈跟踪不同,我不会在调用函数中获得行号,但至少我会有一些东西.
我主要担心的是,即使没有实际抛出异常,这也可能导致速度减慢.所有这些try/catch块是否需要对每个函数调用进行额外的设置和拆除,或者在编译时以某种方式处理?还是有其他问题我还没考虑过?
我认为这是一个非常糟糕的主意.
可移植性是一个非常有价值的目标,但是当它导致一个侵入性,性能下降和低劣实现的解决方案时.
我工作的每个平台(Windows/Linux/PS2/iPhone /等)都提供了一种在发生异常时将堆栈移动并将地址与函数名称匹配的方法.是的,这些都不是可移植的,但报告框架可以是,并且通常需要不到一天或两天的时间来编写特定于平台的堆栈行走代码版本.
这不仅比创建/维护跨平台解决方案的时间短,而且结果要好得多;
无需修改功能
陷阱在标准或第三方库中崩溃
每个函数都不需要try/catch(缓慢且占用大量内存)
抬头Nested Diagnostic Context
一次.这是一个小提示:
class NDC { public: static NDC* getContextForCurrentThread(); int addEntry(char const* file, unsigned lineNo); void removeEntry(int key); void dump(std::ostream& os); void clear(); }; class Scope { public: Scope(char const *file, unsigned lineNo) { NDC *ctx = NDC::getContextForCurrentThread(); myKey = ctx->addEntry(file,lineNo); } ~Scope() { if (!std::uncaught_exception()) { NDC *ctx = NDC::getContextForCurrentThread(); ctx->removeEntry(myKey); } } private: int myKey; }; #define DECLARE_NDC() Scope s__(__FILE__,__LINE__) void f() { DECLARE_NDC(); // always declare the scope // only use try/catch when you want to handle an exception // and dump the stack try { // do stuff in here } catch (...) { NDC* ctx = NDC::getContextForCurrentThread(); ctx->dump(std::cerr); ctx->clear(); } }
开销是在NDC的实施中.我正在玩一个懒惰的评估版本以及一个只保留固定数量的条目的版本.关键点在于,如果你使用构造函数和析构函数来处理堆栈,那么你不需要所有那些讨厌的try
/ catch
块和任何地方的显式操作.
唯一的平台特定头痛是getContextForCurrentThread()
方法.您可以使用线程本地存储来使用特定于平台的实现来处理大多数情况(即使不是所有情况).
如果您更注重性能并且生活在日志文件的世界中,那么更改范围以保存指向文件名和行号的指针并完全省略NDC事物:
class Scope { public: Scope(char const* f, unsigned l): fileName(f), lineNo(l) {} ~Scope() { if (std::uncaught_exception()) { log_error("%s(%u): stack unwind due to exception\n", fileName, lineNo); } } private: char const* fileName; unsigned lineNo; };
抛出异常时,这将在日志文件中为您提供良好的堆栈跟踪.不需要任何真正的堆栈遍历,只是在抛出异常时只需要一条日志消息;)