前一阵子在群里探讨到C++异常上能否应用模板,目前来看答案是不能。但是难道异常就不能更灵活一些么?难道只能try...catch已知的基于类继承体系的异常,对未知的异常re-throw么?(虽然那是经典且正确的方法)经过一番探索,找到了某些黑魔法。
先来看一段代码:
我们的需求是仅捕获 template<typename T> std::pair<T,T&> 这样的异常,由于try-catch无法应用模板,因此能否在catch(...)里获取到这个异常并且在运行时得到其类型名称?
先考虑C++自己能否完成这一任务。<exception>头文件中提供了std::current_exception()函数,能够返回当前的异常. 同时C++还提供了一定的raii功能——typeid,看起来很美好。但是首先,current_exception()返回类型是exception_ptr,是一个指向被抛出异常的指针或引用,除了被用来rethrow_exception之外毫无用处,被抛出的异常类型信息完全丢失。其次typeid并不是一个可靠的工具。在VS下,typeid(T).name()会返回T的字面类型。而在GCC上,typeid(T).name()得到的是一个被编码的(mangled)字符串。(当然GCC也可以用abi::__cxa_demangle来解析并获取真正的类型名)
那么,这个问题在C++自身内就无法得到解决。因此来看不同编译器下如何实现。
GCC的<cxxabi.h>中提供了abi::__cxa_current_exception_type()这样一个函数,能够返回当前正在处理的异常的相关信息,使用abi::__cxa_current_exception_type()->name()可以得到一个被编码的字符串,然后再调用__cxa_demangle进行解码即可获得当前异常的类型字符串。返回的结果非常规整,可以直接操作字符串来实现我们的需求。代码如下:
VS下这个问题显得有一些复杂,首先VS并没有提供cxxabi.h这么好用的内部函数,所以如果只用C++语言设施无法解决问题。但是由于我们在VS下,因而又多了一个新的异常处理系统:__try,__except和__finally。
其中__except接受一个指定格式的函数,用于识别并处理异常。这个函数可以返回三个值:-1 表示忽略当前异常继续运行 0 表示无法匹配异常,系统将继续搜索可能的__except块 1 表示异常已经成功处理完成 (这就跟平时解引用一个空指针弹出来的那个Debug Error对话框差不多)
于是代码变成了这样:
其中filter是一个这样的函数:
其中code是异常代码,__EXCEPTION_POINTERS是一个指向正在处理的异常的结构体(参见MSDN)其中异常代码有很多种,比如EXCEPTION_ACCESS_VIOLATION就是平时访问非法内存(解空指针)的异常代码。然而这里我们要处理的是C++异常,通过探索发现,对于所有C++ throw抛出导致的异常,code都是0xE06D7363,通过搜索资料,确定这是Windows SEH下表示C++异常的代码。
到这里,尽管已经获取到了包含异常信息的结构体,但是仍然无法得到我们想要的类型名。通过搜集资料,找到了一个非常神奇的方法,如下:
_EXCEPTION_POINTERS->ExceptionRecord->ExceptionInformation是一个存储异常参数的数组,其中参数的数量存储在_EXCEPTION_POINRTERS->ExceptionRecord->NumberParameters中。对于C++异常,参数数量总是3或4.
参数0是一些系统使用的内部值,参数1是一个指向被抛出对象的地址的指针,参数2是一个指向被抛出异常的描述信息的指针。如果操作系统是64位的,那么有参数3是一个HINSTANCE值,用于指示是哪一个DLL抛出了异常。
参数2是唯一与异常信息有关的直接信息,然而需要经过一系列的处理才能够得到我们想要的类型名。
首先将参数2当做一个DWORD指针,然后偏移3个DWORD大小,取值。
然后把上一步得到的值当做一个DWORD指针,然后偏移1个DWORD大小,取值。
接下来把上一步得到的值当做一个DWORD指针,然后偏移1个DWORD大小,取值
最后把上一步得到的值当做一个char指针,然后偏移2个void指针大小。这个字符串就是我们要获取的类型名。
(注:当以64位编译时需要把每个步骤取到的值加上参数3的那个HINSTANCE才行)
语言描述看起来可能比较复杂,来看代码:
在我的电脑上,运行得到的类型名是:.?AU?$pair@HAAH@std@@
令人开心的是,我们得到了一些结果。然而,我们并不知道这个结果是什么。不过直觉判断这应该就是编码过后的类型名。
这里,需要了解一下typeid(T).raw_name()。这个函数返回T类型编码后的字符串。运行typeid(std::pair<int,int&>).raw_name() 获取到的结果是:.?AU?$pair@HAAH@std@@ 完全一致。
那么如何从这个奇怪的字符串回到std::pair<int,int&>这样的格式呢?VS只为我们提供了一个功能很有限的函数:UnDecorateSymbolName(具体用法参见MSDN) 说他功能有限,是因为这个函数本质上是通过查找一个内置的表格来获取对应的结果,而不是想象中的动态逆向解析符号串。因此解析正确率完全取决与库里是否有待查找的符号。从而我们的代码最终变成了:
不知道是不是因为模板的复杂性,这个函数基本上对模板无效。对我们的得到的编码串调用UnDecorateSymbolName并不能得到正确的结果(只会返回原来的字符串,进一步表示无法解析符号串)从而我们还是无法达到最开始的需求(无奈)。
不过,值得一提的是,同样运行在Windows下的MinGW-GCC通过其abi函数库是能够达到与Linux下GCC一样的运行效果的。如果不是提前存储了类型名,那么肯定还是有某种方法能够从这堆编码串反推出实际类型名称。