一个C程序能够处理C++异常吗?

17

我正在开发一个C++组件dll,可以被C或C++应用程序使用。暴露的dll函数如下:

#include <tchar.h>
#ifdef IMPORT
#define DLL __declspec(dllimport)
#else 
#define DLL __declspec(dllexport)
#endif

extern "C" {

    DLL  bool __cdecl Init();
    DLL  bool __cdecl Foo(const TCHAR*);
    DLL  bool __cdecl Release();

}

这些函数的内部实现是C ++类,但不会公开。假设使用这种风格,dll可以用于C或C++应用程序。 问题在于我没有处理任何类型的C++异常(例如bad_alloc),而是将此任务留给了调用方(更高层次的代码)。 经过与同事的激烈讨论,他们认为我应该捕获所有异常并返回错误代码,或者至少返回false,因为对于C应用程序来说无法处理C++异常?这是真的吗?一般情况下我应该怎么做?如果你正在开发将由其他系统使用的组件,有什么通用的异常处理规则吗?


2
不,C语言中没有异常这种东西。 - sje397
3
C++可以通过std::set_unexpected()函数将C风格的/具有extern C链接的函数注册为未捕获异常的异常处理程序。如果已知该函数的名称修饰符,您可以直接从纯C代码中调用它。否则,您可以公开一个extern C包装器函数。需要注意的是这并不是一个方便的界面,但如果绝对避免不了库污染其它范围之外的内容,这种方法也是可行的。 - FrankH.
@FrankH:这应该是一个答案,而不是评论。许多库(例如遗留的Fortran库)使用处理程序方法。 - Alexandre C.
1
@DumbCoder:在真正的代码中不要这样做。 - Alexandre C.
在实际实验中,我需要使用 std::set_terminate() 而不是 std::set_unexpected() 来使 C++ 运行时重定向到由 C 提供的函数。以下是示例。 - FrankH.
显示剩余2条评论
5个回答

20

C语言没有异常机制,因此通常应该捕获所有异常并返回错误码和/或提供一个函数来返回有关最后一个错误的信息。


"C语言没有异常处理机制。为什么呢?C++运行时有异常处理机制,而且C与C++运行时进行接口交互时,C++运行时也会存在。因此,为什么C不能截取特定实现的C++运行时异常处理机制呢(不同系统可能有不同的实现,但实现仍然存在于内存中,即使它没有暴露出来,您也可以分析内存以找出它并调用未公开的函数来操作不同的字节)"。 - Dmytro

19
如果使用MSVC,那么是可以在C中捕获异常的,但是效果不会很好。C++异常是通过操作系统ABI的结构化异常处理机制传递的,并且Microsoft有一个__try,__except,__finally C扩展来处理OS结构化异常。请注意,这些包括访问冲突,除以零等错误,通常您会想要终止程序并记录错误报告。您可以通过代码0xE04D5343(4D 53 43 =“MSC”)识别C++异常,然后将其抛出。

尽管如此,您可能不希望在DLL边界上抛出异常,特别是如果您只公开了C API。


如果我的dll客户端是C++应用程序,是否在dll边界传播异常是个好主意呢? - Ahmed
1
@Ahmed:不,除非你想为每个编译器版本提供DLL。 - Georg Fritzsche
@Ahmed:在这种情况下,我会说是的,但是很容易解决-->如果您提供C接口,请不要使用异常,如果您提供C++接口,则可以使用。 - Matthieu M.
1
@Georg:啊,我总是假设客户会重新编译源代码;p - Matthieu M.

13
作为一般规则,您应该绝不允许C++异常超出模块边界传播。这是因为C++标准没有指定如何实现异常传播,因此这取决于编译器(和编译器标志)以及操作系统。您无法保证调用您的模块的代码将使用与您的模块相同的编译器和编译器标志进行编译。实际上,正如您在此问题中所展示的那样,您无法保证调用您的模块的代码将使用相同的语言编写。
有关更多详细信息,请参阅Sutter和Alexandrescu的《C++编码标准》中的第62项。

9

好的,既然被要求了:

C++示例代码:

#include <typeinfo>
#include <exception>
extern "C" {
void sethandler(void (*func)(void)) { std::set_terminate(func); }
int throwingFunc(int arg) {
    if (arg == 0)
        throw std::bad_cast();
    return (arg - 1);
}
}

C示例代码:

#include <stdio.h>

extern int throwingFunc(int arg);
extern void sethandler(void (*func)(void));

void myhandler(void)
{
    printf("handler called - must've been some exception ?!\n");
}

int main(int argc, char **argv)
{
    sethandler(myhandler);

    printf("throwingFunc(1) == %d\n", throwingFunc(1));
    printf("throwingFunc(-1) == %d\n", throwingFunc(-1));
    printf("throwingFunc(0) == %d\n", throwingFunc(0));
    return 0;
}

当我编译这两个文件,将它们链接在一起并运行它们时(Ubuntu 10.04,gcc 4.4.5,x64),我得到以下结果: $ ./xx throwingFunc(1) == 0 throwingFunc(-1) == -2 handler called - must've been some exception ?! Aborted
所以,虽然你可以从C中捕获异常,但这远远不够——因为C++在std::terminate()之后的运行时行为是未定义的,并且处理程序没有任何状态信息(以区分异常类型和/或源)。处理程序无法清理任何内容。
顺便说一下,我故意选择了std::bad_cast()作为此示例中的异常类型。这是因为抛出该异常类型可以说明std::set_unexpected()和std::set_terminate()之间的行为差异——对于所有非std::bad_*异常,都会调用意外处理程序,而为了捕获标准异常,则需要终止处理程序……看到困境了吗?这个工具过于宽泛,无法实际使用 :(

实际上这样做的目的只是在终止之前向用户提供错误消息。关键是必须捕获应该捕获的内容,并对其余的异常进行尽可能详细的描述。 - Alexandre C.
这就是为什么我说“两难” - 你可以捕获异常并记录错误,但是“尽可能详细”的描述有多详细呢?从处理程序没有传递任何上下文的事实来看,我会说“不太”... - FrankH.

7
只有当调用者设计为处理异常时,才将异常传播给调用者。我猜在你的情况下,如果任何异常逃离了C++代码,terminate()将立即被调用,因为从C++运行时的角度来看,该异常尚未被处理。
在COM服务器设计中也会出现同样的情况-客户端可以使用任何语言/技术。规则是不应该使任何异常逃脱COM服务器方法-必须捕获所有异常并将其转换为HRESULT和(可选)IErrorInfo。您应该在自己的情况下采取类似的做法。
如果C代码夹在两个C++代码层之间,将异常传播到C代码仍然是一个非常糟糕的想法

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