【问题标题】:C++ API design and error handlingC++ API 设计和错误处理
【发布时间】:2012-02-21 16:15:45
【问题描述】:

我需要使用 .lib 文件 (MSVC) 编写 C++ API,它由几个从 Dll 公开的导出 C++ 类组成。根据我对另一个问题的回答,我理解,如果 C++ API 是在一个 VC++ 版本(比如说 2010 年)中构建的,并且客户端代码是用另一个 VC++ 版本编写的,那么导出的类方法不能使用异常。由于异常不能成为公共 API 接口的一部分,因此我正在寻找另一种错误处理策略。我的限制:我不想使用 COM,丰富的错误代码系统(如 HRESULT)对我来说还不够。我想要一个类异常类,其中包含错误代码、错误消息和我需要的任何其他信息。另外,我不想为每个 VC++ 版本单独构建。

我目前的方法如下。每个公共类方法都返回枚举值(如 ErrorCode)。在方法失败的情况下,像 GetLastErrorInfo 这样的静态函数会返回指向 C++ 类的指针(比如说 ErrorInfo),其中包含到达错误信息。 ErrorInfo 保存为线程特定的数据,并包含来自当前线程中最后一次调用的错误信息。如果最后一次 API 调用成功,GetErrorInfo 返回 NULL。

考虑这段代码的例外情况:

尝试 { classPtr->DoSomething(); cout GetData()

无一例外都是这样的:

错误码结果; 整数数据; 结果 = classPtr->DoSomething(); 如果(结果!=成功) { cout GetErrorMessage()GetData(data); 如果(结果!=成功) { cout GetErrorMessage()

这看起来不太好。类接口很乱:现在每个函数都有 ErrorCode 返回类型。返回值成为输出参数。有没有更好的方法,可以获取错误信息,并保持公共 API 接口干净?

【问题讨论】:

  • 您可以使用 boost::tuple 来拥有多种返回类型并避免整个输出参数的事情。但这只是解决真正问题的技巧
  • 另外,如何创建解释错误代码并引发异常的内联包装函数。我可以想到两种方法来做到这一点。 1.客户端做包装器(myFunction)。 2.client做myFunction,myFunction是wrapper(internalMyFunction)里面。使用元组和模板元编程通过包装器传递其他返回类型。
  • 可能的解决方案是在内部保留最后一个错误,每个方法都会更新它。因此,类方法不需要返回错误。还有一种方法可以访问最后一个错误代码。当然,多线程访问的情况下解决方案会更复杂
  • 懒得写真正的答案,不过ideone.com/ONYgz
  • @Eugene - 我正在考虑这个问题。对于多线程,我想使用线程特定的数据(Windows 术语中的 TLS)。

标签: c++ api exception error-handling


【解决方案1】:

如果您从 .dll 返回 C++ 对象,您必须非常小心,因为您的调用者可能会尝试以涉及复制或删除它们的方式使用这些对象。如果调用者没有使用相同的堆(或相同的标准库),那么您将到处发生各种崩溃和内存泄漏。根据我的经验,通过 DLL 边界传递 C++ 对象是个坏主意。

我会尝试以一种要求调用者管理所有内存(分配和解除分配)的方式创建 API,以便始终明确指针的所有权。这会产生更多的工作,因为您需要接受指针,然后用数据填充这些缓冲区,而不是返回 C++ 对象。我还没有看到在 .dll 之间安全地传递 C++ 对象的工作实现。 (但那我的经验可能有限。)

此外,当您从 .dll 导出 C++ 类时,除了 C++ 之外的任何东西都不太可能使用该 .dll,因为其他语言具有不同的对象内存布局。即使是不同的 C++ 编译器在使用这些类时也可能会出现问题。

重新阅读您的约束,我想说创建 COM 对象是您最好的选择之一,因为它为您提供了灵活性和返回具有复杂数据的对象(尽管不是 C++ 对象)的能力。

【讨论】:

  • .. 微软又把它弄得一团糟! :-(
【解决方案2】:

这个怎么样:

基本错误数据 - 您可以扩展您的库的“其他内容”:

namespace MON {
  class t_error_description {
  public:
    t_error_description(const int& code, const std::string& message);
    virtual ~t_error_description(); /* << allow any other info via subclass */
  public:
    virtual void description(std::ostream& stream) const;
    /* … */
  private:
    const int d_code;
    const std::string d_message;
  };
}

基本错误容器。包装与描述相关的所有内容,这是客户直接处理的所有内容:

namespace MON {
  class t_error {
  public:
    t_error();
    ~t_error();
  public:
    /* or perhaps you'd favor a stream op? */
    void description(std::ostream&) const;
    /* sets the error - this is a take operation */
    void set(const t_error_description* const desc);

    void clear();
    /* … */
  private:
    /* trivial construction */
    t_auto_pointer<const t_error_description> d_errorDescription;
  private:
    /* verboten */
    t_error(const t_error&);
    t_error& operator=(const t_error&);
  };
}

基本库调用:

namespace MON {
  /* return false on error */
  bool DoSomething(t_error& outError) {
    if (Foo()) {
      outError.set(new t_error_description(ErrorCodeThingy, "blah blah"));
      return false;
    }
    return true;
  }
}

客户电话:

MON::t_error err;
if (!MON::DoSomething(err)) {
  log << "cannot do anything!\nError: ";
  err.description(log);
  return;
}

【讨论】:

    【解决方案3】:

    您可能忽略了一个简单的解决方案。唯一的限制是异常不能跨越模块边界。客户端代码本身抛出异常没有问题。所以在头文件中提供一个内联函数,比如 CheckReturn(),它会抛出丰富的异常。

    要获得灵感,请查看 COM IErrorInfo 接口及其关联的_com_error class。他们解决了完全相同的问题。还要注意 MSVC 中可用的 #import 指令,它会自动生成小的包装函数,这些函数会进行调用并在失败返回值时抛出异常。但是你不想使用 COM,所以它不能直接使用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-12-05
      • 2014-02-23
      • 1970-01-01
      • 2021-11-29
      • 1970-01-01
      • 2013-08-27
      • 2016-12-05
      • 1970-01-01
      相关资源
      最近更新 更多