【问题标题】:Can a C program handle C++ exceptions?C 程序可以处理 C++ 异常吗?
【发布时间】:2011-05-25 19:44:21
【问题描述】:

我正在开发可供 C 或 C++ 应用程序使用的 C++ 组件 dll。 暴露出来的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),我把这些东西留给了调用者(更高层)。 在与我的同事多次辩论后,我应该捕获所有异常并返回错误代码或至少返回错误,因为在 C 应用程序的情况下它不能处理 C++ 异常?真的吗?我一般应该怎么做?如果您正在开发将被其他系统使用的组件,是否有处理异常的经验法则。

【问题讨论】:

  • 不,C 中没有异常之类的东西。
  • C++ 可以通过std::set_unexpected() 注册一个 C-linkage / extern C 函数作为未捕获异常的异常处理程序。如果知道它的损坏名称,您可以直接从普通 C 调用它。否则,您可以为其公开一个 extern C 包装器。并不是说这是一个方便的界面,但如果您的图书馆在其自身边界之外造成污染是绝对不可避免的......
  • @FrankH:这应该是一个答案,而不是评论。许多库(例如旧版 fortran 库)使用处理程序方法。
  • @DumbCoder:不要在实际代码中这样做。

标签: c++ c exception-handling


【解决方案1】:

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

【讨论】:

  • “C 没有异常”。为什么不; C++运行时有异常,而C与C++运行时的接口有C++运行时,那为什么C不能针对异常的具体实现截取C++运行时(不同的系统可能有不同的实现,但实现还是存在的,必须存在于内存,即使没有暴露,也可以分析内存找出来,戳不同的字节,调用未暴露的函数)。
【解决方案2】:

如果这是使用 MSVC 的 Windows,那么是的,您可以在 C 中捕获异常,但不能很好地捕获它们。 C++ 异常通过 OS ABI 的结构化异常处理机制提供,Microsoft 有一个 __try, __except, __finally C 扩展来处理 OS 结构化异常。请注意,这些包括访问冲突、被零除等,您通常希望终止程序并记录错误报告。您可以通过代码 0xE04D5343 (4D 53 43 = "MSC") 识别 C++ 异常,然后将其余的抛出。

总而言之,您可能不希望跨 DLL 边界引发异常,如果您只是公开 C API,当然也不会。

【讨论】:

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

仅当调用者被设计为处理异常时才将异常传播给调用者。我猜在你的情况下terminate() 将在任何异常从 C++ 代码中转义后立即被调用,因为从 C++ 运行时的角度来看,该异常尚未得到处理。

在 COM 服务器设计中也会出现同样的情况 - 客户端可以使用任何语言/技术。规则是任何异常都不应转义 COM 服务器方法 - 所有异常都必须在 HRESULT 和(可选)IErrorInfo 中捕获和翻译。你应该在你的情况下做类似的事情。

如果C代码夹在两层C++代码propagating exceptions to C code is still a very bad idea之间。

【讨论】:

    【解决方案4】:

    作为一般规则,您应该永远允许 C++ 异常传播到模块边界之外。这是因为 C++ 标准没有指定如何实现异常传播,因此这取决于编译器(和编译器标志)和操作系统。您不能保证调用您的模块的代码将使用与您的模块具有相同编译器标志的相同编译器进行编译。事实上,正如您在这个问题中所展示的那样,您不能保证调用您的模块的代码将以相同的语言编写。

    更多详情,请参阅 Sutter 和 Alexandrescu 的 C++ 编码标准中的第 62 条。

    【讨论】:

      【解决方案5】:

      好的,因为它被要求:

      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
      处理程序调用 - 一定是一些例外?!
      中止
      

      因此,虽然您可以从 C 中捕获异常,但这还不够 - 因为 std::terminate() 之后的 C++ 运行时行为未定义,并且因为处理程序没有任何状态信息(用于区分异常类型和/或来源)。处理程序无法清理任何东西。

      顺便说一句,在这个例子中我特意选择了std::bad_cast() 作为异常类型。这是因为 throw 说明了 std::set_unexpected()std::set_terminate() 之间的行为差​​异 - 将针对所有非 std::bad_* 异常调用意外处理程序,而为了捕获标准异常,需要终止处理程序...参见困境?线束太宽,无法实际使用:(

      【讨论】:

      • 实际上这样做的目的只是为了在中止之前向用户提供错误消息。关键是必须捕获应该捕获的内容,并且对于剩余的异常,尽可能具有描述性。
      • 这就是我说“困境”的原因——您可以捕获异常并记录错误,但描述性如何尽可能描述性?从处理程序没有传递任何上下文的事实来看,我会说“不是非常”......
      猜你喜欢
      • 2011-01-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-11
      相关资源
      最近更新 更多