C++已经具备某种反射功能了吗?

6
考虑以下例子:
struct Nobody_Expects_The_Spanish_Inquisition{};

int main(){
    throw Nobody_Expects_The_Spanish_Inquisition();
}

Ideone上显示的输出:

抛出一个Nobody_Expects_The_Spanish_Inquisition实例后终止。

Windows系统的类似输出:

在Test.exe的0x760fb727处未处理的异常: Microsoft C++ 异常: Nobody_Expects_The_Spanish_Inquisition,内存位置为0x001ffea3.

可以看出,最终汇编似乎已经包含了异常名称,或者存在另一种获取名称的方法。

这是否可以视为某种反射?或者,如果异常名称实际上可以显示,那么它是编译器/操作系统相关的吗?


3
@Damon,我认为这与什么无关,因为原帖的问题没有使用继承自std :: exception的异常。 - user2100815
1
反射通常被定义为程序使用编程语言特性自省其自身部分的能力。未处理的运行时异常似乎不符合这些要求之一,因此我不明白你是如何从这个例子得出“反射”的结论的。嵌入在对象文件中的调试文本字符串不计算在内。 - Ira Baxter
每当提到“C++和反射”时,我总是有点惊讶,没有人甚至提到typeid / RTTI。 - Nemo
1
@Nemo 我想不出你为什么要在这个上下文中提到它们 - 它们与异常无关。 - user2100815
@Damon: 你应该首先捕获 std::exception,然后作为最后的手段捕获所有异常。如果你恰好捕获到一个 std::exception,那么你可以利用 std::exception::what 成员函数来记录更有意义的错误信息。 - Emile Cormier
显示剩余9条评论
5个回答

7

这取决于编译器。显然,编译器很容易发现每个throw,并将每个抛出的对象的类型编码到可执行文件中。但是并没有要求他们这样做。

考虑一下,当异常被抛出时,它们必须被复制到一个奇怪的实现相关空间中。因此,通过这种机制让特定编译器的运行时可以访问它们的类型名称是有意义的。


1
@Omnif 很可能不行。如果你要打负分,至少要有勇气公开表态。 - user2100815
@Neil Butterworth:好的,那我给你点个踩。因为我认为还有其他答案(例如@Luc Danton的答案)比这个更好。 - Omnifarious
1
@Neil Butterworth:这个回答几乎完全以更清晰、更直接的方式回答了OP的问题。它唯一缺少的是指出RTTI是一种非常原始的反射形式,而这只是一个相对较小的问题。这个答案完全避开了关于反射的问题,只回答了终止处理程序如何知道它正在打印的信息,然后用一种不太清晰的方式回答了这个问题。 - Omnifarious
2
@Omni:因为有更好的答案而打-1?这是不合理的。只有当答案建议了不良实践或完全错误时,才应该给出-1,而不是因为认为答案不是最好的。拜托了... - Xeo
@Xeo:完全正确。您可以通过比较分数来确定最受欢迎的答案。当然,您绝对不应该对那些不是您最喜欢的答案进行投反对票。 - Lightness Races in Orbit
显示剩余2条评论

3
不,它并不是实质意义上的反射,只是调试符号。

1
实际上,使用MinGW gcc编译时,如果你在没有调试的情况下剥离可执行文件,仍然会在崩溃时得到符号名称。我同意这并不是反射。 - user2100815
正如@Neil所说,即使在没有调试符号的发布模式下,名称仍然会被显示。 - Xeo
1
不,我不认为我会这样做。符号名称只有在调试时对开发人员有价值。这使得它成为调试符号。仅仅因为它没有编译成单独的文件供进程内调试器检查,并不意味着它不是调试符号 - 实际上,除了调试之外,它们对任何目的都是无用的。 - Puppy
@Dead 我没有使用-g标志编译并剥离了可执行文件。没有调试符号。事实上,当使用-O2和剥离编译时,程序崩溃时仍然会得到相同的符号名称,这几乎就是一个非调试的可执行文件。如果您知道如何删除异常的符号名称,请告诉我。 - user2100815
如果答案修改为例如“调试信息”而不是“调试符号”,这种暴力协议就可以停止,因为“调试符号”确实具有非常特定的含义。 - Luc Danton
显示剩余2条评论

2
当异常逃逸出main时,将调用std::terminate,进而调用已安装的终止处理程序。如果程序中未设置终止处理程序,则调用默认终止处理程序。这个默认终止处理程序的唯一要求是调用std::abort。这意味着实现可以在调用std::abort之前打印有用的消息,这显然是本例的情况。
虽然反射、调试符号或RTTI足以打印此错误消息,但它们并非必需品:实现可以使用任何类型的黑魔法,无论多么深奥。

0

这个程序:

#include <typeinfo>
#include <iostream>

struct Nobody_Expects_The_Spanish_Inquisition {
};

namespace junk {
struct I_didnt_expect_a_kind_of_spanish_inquisition
   : public Nobody_Expects_The_Spanish_Inquisition
{
};
}

int main(int argc, const char *argv[])
{
   using ::std::type_info;
   using ::std::cout;

   Nobody_Expects_The_Spanish_Inquisition foo;
   junk::I_didnt_expect_a_kind_of_spanish_inquisition bar;
   const type_info &fooinfo = typeid(foo);
   const type_info &barinfo = typeid(bar);
   cout << "The type of foo is <" << fooinfo.name() << ">\n";
   cout << "The type of bar is <" << barinfo.name() << ">\n";
   return 0;
}

有以下输出:

$ ./foo 
The type of foo is <38Nobody_Expects_The_Spanish_Inquisition>
The type of bar is <N4junk44I_didnt_expect_a_kind_of_spanish_inquisitionE>

在C++中,这是内省的最佳方法。就连默认的终止处理程序都勉强能够完成该任务。

正如其他人所指出的那样,虽然默认终止处理程序可以随意使用任何方式来完成此目标,但如果它没有使用与实现typeid相同的机制,则让人惊讶。

当然, 默认终止处理程序可以通过访问编译器在抛出异常时创建的特殊区域来记录编译器在抛出异常的地方所知道的类型名称。正如其他人所指出的那样,默认终止处理程序由编译器放置,并不受任何C++程序员编写的代码必须遵循的规则的限制。

我曾经见过人们编写自己的终止处理程序,手动遍历调用堆栈并查找与每个地址相关联的调试符号,以获得堆栈跟踪的近似效果。这些都是编译器和平台特定的魔法,由于编译器确切地知道它是哪个编译器以及正在使用哪个平台,因此它可以在支持的平台上拥有一个默认的终止处理程序来完成相同的操作。

总之,实现您所提到的功能不需要RTTI。但RTTI是一种非常基础的反射形式,可以用来实现该功能。


重点是,至少对于GCC编译器来说,它会为你完成所有这些工作。 - user2100815
@Neil Butterworth:确实如此。但它很可能只是通过使用现有的typeid框架来找出异常类型,从而使默认的“糟糕,main以异常终止!”代码实现了这一点。 - Omnifarious

-1
“反射”是使用RTTI实现的。没有什么花哨的东西,只是类型的名称而已。

1
禁用了RTTI,仍然可以获取异常名称,因此没有使用RTTI。 - Xeo
@Xeo:仅仅因为你“禁用了RTTI”并不意味着typeid部分没有为作为异常抛出的类型设置。基本上,为了使打印名称的代码工作,它必须存在。 - Omnifarious

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接