使用 CoInitialize 和 CoUninitialize 抛出 C++ 异常

Throwing C++ exceptions with CoInitialize and CoUninitialize

我有这样的功能:

bool op1();
bool op2();

bool foo() {
  CoInitialize(nullptr);
  if (!op1()) {
    CoUninitialize();
    return false;
  }
  // do more stuff, then call op2...
  if (!op2()) {
    CoUninitialize();
    return false;
  }
  // happy path
  CoUninitialize();
  return true;
}

我想重构 foo() 以抛出异常:

void foo() {
  CoInitialize(nullptr);
  if (!op1()) {
    CoUninitialize(); // I'm lazy, can't I automate this call?
    throw std::exception("Failed");
  }
  // ...

但是我每次出错都得调用CoUninitialize()

我考虑过将 COM init 调用包装在 class 中,因此析构函数 will do the cleanup,但是有一个未使用的对象在我看来很奇怪:

class comlib {
public:
  comlib() { CoInitialize(nullptr); }
  ~comlib() { CoUninitialize(); } // automated!
};

void foo() {
  comlib nobodyEverCallsMe;
  if (!op1()) {
    throw std::exception("Failed");
  }
  // ...

有更好的方法吗?

Raymond Chenusing this method已经有一段时间了所以我确定没问题,只要记得只打电话给CoUninitialize如果CoInitialize SUCCEEDED!

class CCoInitialize {
  HRESULT m_hr;
public:
  CCoInitialize() : m_hr(CoInitialize(NULL)) { }
  ~CCoInitialize() { if (SUCCEEDED(m_hr)) CoUninitialize(); }
  operator HRESULT() const { return m_hr; }
};


void Something()
{
  CCoInitialize init;
  ...
}

有些人可能想在 CoInitialize 失败时抛出构造函数,但我觉得这是不必要的,因为其他 COM 调用会失败。仅当您需要从 CoInitialize 中捕获准确的 HRESULT 失败代码时才这样做。

Is there a better approach to this?

没有

使用 RAII (Responsibility1 Acquisition is Initialization) 习惯用法在退出时进行清理是针对您要解决的问题的标准 C++ 解决方案。如果是 COM,我会建议以下实现:

#include <Windows.h>
#include <comdef.h>

struct com_init
{
    com_init()
    {
        HRESULT hr{::CoInitialize(nullptr)};
        if (FAILED(hr))
        {
            throw _com_error{hr};  // _com_error is declared in comdef.h
        }
    }
    com_init(com_init const&) = delete;
    com_init& operator=(com_init const&) = delete;

    ~com_init()
    {
        ::CoUninitialize();
    }
};

您的示例中使用的是这样的:

void foo()
{
    com_init guard{};

    if (!op1())
        throw std::exception{"Failed"};
    // ...

使用 C++17 可以将对象标记为 [[maybe_unused]],以防止编译器警告并传达意图。

理由:

该实现使用了一个在失败时抛出异常的构造函数。这样做有很多充分的理由:

  • Space:此实现不需要存储任何状态信息,以允许析构函数有条件地执行清理。
  • 一致性:C++ 是围绕异常构建的。将基于异常的代码与通过错误代码报告错误的代码交织在一起既令人困惑又难以理解。
  • 可靠性:未能初始化 COM 是灾难性的故障。没有想出的方法可以从中恢复。它不能被忽视。您不能忽略异常。错误代码的默认设置是忽略它们;您无需为此做任何事情。


1 通常标记为 "Resource",但这并不完全符合它提供的多功能性。我更喜欢"Responsibility".