将GetLastError()转换为异常

6

我有一个使用Win32Exception类的Visual Studio 2008 C++项目,当出现异常错误时会使用该类。 Win32Exception类如下所示:

/// defines an exception based on Win32 error codes. The what() function will
/// return a formatted string returned from FormatMessage()
class Win32Exception : public std::runtime_error
{
public:
    Win32Exception() : std::runtime_error( ErrorMessage( &error_code_ ) )
    {
    };

    virtual ~Win32Exception() { };

    /// return the actual error code
    DWORD ErrorCode() const throw() { return error_code_; };

private:

    static std::string ErrorMessage( DWORD* error_code )
    {
        *error_code = ::GetLastError();

        std::string error_messageA;
        wchar_t* error_messageW = NULL;
        DWORD len = ::FormatMessageW( FORMAT_MESSAGE_FROM_SYSTEM | 
                                      FORMAT_MESSAGE_ALLOCATE_BUFFER |
                                      FORMAT_MESSAGE_IGNORE_INSERTS,
                                      NULL,
                                      *error_code,
                                      MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ),
                                      reinterpret_cast< LPWSTR >( &error_messageW ),
                                      0,
                                      NULL );
        if( NULL != error_messageW )
        {
            // this may generate a C4244 warning. It is safe to ignore.
            std::copy( error_messageW, 
                       error_messageW + len, 
                       std::back_inserter( error_messageA ) );
            ::LocalFree( error_messageW );
        }
        return error_messageA;
    };

    /// error code returned by GetLastError()
    DWORD error_code_;

}; // class Win32Exception

这个类在它被使用的情况下表现良好。我想知道是否有任何明显的情况会导致失败,以便我可以注意到。欢迎提供其他注意事项、警告或改进建议。

请注意,boost库不是此代码的选项。


如果你在想,这个类也被用于没有 FormatMessageA 的 WindowsMobile。这就是为什么它从UNICODE转换为ASCII的原因。 - PaulH
6个回答

6
请注意,如果back_inserter导致抛出std::bad_alloc,则在FormatMessage内部分配的内存将无法释放。

1
我错过了一个内存泄漏,但是你发布的链接代码不是线程安全的,它使用静态变量来存储what()函数的结果。 - Yakov Galka
@ybungalobill:是的,它不是线程安全的——我只在单线程应用程序中使用它。(并且缺乏线程安全性在紧挨着它的注释中已经明确标注了:P)如果您需要线程安全性,可以将std::string存储在异常对象本身内部,但我选择不这样做是为了避免在抛出异常时支付std::string构造的代价。如果您需要线程安全,请调用GetCharMessage(),它返回一个std::string - Billy ONeal
@ybungalobill:但是即使你不需要使用错误消息,你仍然需要支付构建std::string的费用。 - Billy ONeal
同意。但是一个空字符串可能会更便宜一些。如果这真的是个问题,你可以尝试存储一个指向堆分配字符串的可变指针。 - Yakov Galka
1
@TamásSzelei:已更新。谢谢! - Billy ONeal
显示剩余3条评论

3
  • What a coincidence! I use a similar code in all my projects! It is actually a good idea.

  • This code is problematic:

        // this may generate a C4244 warning. It is safe to ignore.
        std::copy( error_messageW, 
                   error_messageW + len, 
                   std::back_inserter( error_messageA ) );
    

    It just trancates WCHARs to chars. Your can either use FormatMessageA explicitly to get a message in the current code-page (ok, you can't as you said), or make convention that all your stings are UTF-8 encoded. I chose the later, see this why.

  • Error message by itself may be not useful. Capturing the stack trace may be a good idea.


std::runtime_error派生自std::exception,后者定义了what()函数为const wchar_t* what()。这就是为什么我将字符串转换为ASCII码的原因。或者你是说我应该使用其他方法进行字符串转换而不是std::copy()?我可以转而使用::wcstombs(),但我认为这种方法在当时似乎更容易些。 - PaulH
使用wcstombs、WideCharToMultiByte或像现在一样复制,并验证文本实际上是ASCII。虽然这并不关键,因为FormatMessage在任何正常的系统上可能返回ASCII。 - Yakov Galka
@PaulH:它是从what传递的const char *,而不是wchar_t(我希望它可以这样工作)。使用wcstombs的困难在于您不知道它期望的源编码,也不知道它转换为什么编码。考虑到您正在处理特定于Win32的情况,您可能只需使用FormatMessageA - Billy ONeal
@Billy:OP在他的贴子评论中说他无法在WindowsMobile上使用FormatMessageA。 - Yakov Galka
@ybungalobill:有趣。那就用WideCharToMultiByte吧。 - Billy ONeal
@ybungalobill:抱歉...我以为我已经+1了它。+1。另外,如何捕获堆栈跟踪? - Billy ONeal

2

虽然这篇文章有点老,但至少在使用 VC++ 2015 的时候,你可以抛出一个 system_error ,它能够使用 system_category() 函数完成所有这些功能:

try
{
    throw system_error(E_ACCESSDENIED, system_category(), "Failed to write file");
}
catch (exception& ex)
{
    cout << ex.what();
}

这会打印出:「无法写入文件:拒绝访问」。

1
  • FormatMessage 可能会失败。对于这种情况,一些中性的“未知错误,代码为 %d”可能是必要的。
  • 有些错误代码实际上并不是错误(例如 ERROR_ALREADY_EXISTS),这取决于用户的期望。
  • 一些系统函数返回它们自己的错误代码(一个显著的例子是 SHFileOperation),你必须单独处理它们。如果你想要它们被处理的话。
  • 考虑在异常中添加额外的信息:异常抛出的位置(源文件和行号),导致异常的系统函数,函数的参数是什么(至少是标识参数,如文件名、句柄值等)。堆栈跟踪也很好。

1
<nitpicker corner>“代码%d出现未知错误”实际上我认为应该是%u,或者0x%8x更酷炫。</nitpicker corner> - Matteo Italia

1
我想知道的是,是否存在任何明显的情况会导致失败,我应该注意。欢迎提出任何其他需要注意的问题、警告或改进建议。
我在这种消息检索中遇到的主要问题是ERROR_SUCCESS。当某些操作失败时,伴随着错误消息“操作成功”,这是相当令人困惑的。人们不会认为这种情况会发生,但它确实发生了。
我想这是Dialecticus所指出的一个特殊情况,即“有些错误代码实际上并不是错误”的一个例子,但对于大多数这些代码,至少消息通常是可以接受的。
第二个问题是,大多数Windows系统错误消息在末尾都有回车+换行符。这对于将消息插入到其他文本中是有问题的,并且它打破了C++异常消息的约定。因此,最好删除这些字符。
现在,除了重复其他人已经指出的所有内容之外,再谈一下设计。
如果将ErrorMessage函数公开或移出类,并按值获取错误代码而不是获取指针参数,则该函数将更加可用。这是保持不同职责分离的原则。促进重用。

ErrorMessage中的代码如果使用析构函数来释放内存,将更加清晰、安全和高效。然后您还可以直接在return语句中构造字符串,而不是使用带有后插入器的复制循环。

祝好!


0

最近我在处理一个非常类似的类,读完这个线程后,我试图使复制部分具有异常安全性。我引入了一个小助手类,它仅仅是保存由::FormatMessage返回的字符串指针,并在其析构函数中使用::LocalFree释放它。不允许复制、赋值和移动,因此不会出现问题。

以下是我总结出来的全部内容:

class windows_error {
public:
    windows_error(wchar_t const* what);

    // Getter functions
    unsigned long errorCode() const { return _code; }
    wchar_t const* description() const { return _what; }
    std::wstring errorMessage() const { return _sys_err_msg; }
private:
    unsigned long _code;
    wchar_t const* _what;
    std::wstring _sys_err_msg;
};

// This class outsources the problem of managing the string which
// was allocated with ::LocalAlloc by the ::FormatMessage function.
// This is necessary to make the constructor of windows_error exception-safe.
class LocalAllocHelper {
public:
    LocalAllocHelper(wchar_t* string) : _string(string) { }
    ~LocalAllocHelper() {
        ::LocalFree(_string);
    }

    LocalAllocHelper(LocalAllocHelper const& other) = delete;
    LocalAllocHelper(LocalAllocHelper && other) = delete;
    LocalAllocHelper& operator=(LocalAllocHelper const& other) = delete;
    LocalAllocHelper& operator=(LocalAllocHelper && other) = delete;

private:
    wchar_t* _string;
};

windows_error::windows_error(wchar_t const* what)
    : _code(::GetLastError()),
      _what(what) {
    // Create a temporary pointer to a wide string for the error message
    LPWSTR _temp_msg = 0;
    // Retrieve error message from error code and save the length
    // of the buffer which is being returned. This is needed to 
    // implement the copy and assignment constructor.
    DWORD _buffer_size = ::FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | 
                                          FORMAT_MESSAGE_FROM_SYSTEM |
                                          FORMAT_MESSAGE_IGNORE_INSERTS, 
                                          NULL, _code, 0, _temp_msg, 0, NULL);

    if(_buffer_size) {
        // When calling _sys_err_msg.resize an exception could be thrown therefore
        // the _temp_msg needs to be a managed resource.
        LocalAllocHelper helper(_temp_msg);
        _sys_err_msg.resize(_buffer_size + 1);
        std::copy(_temp_msg, _temp_msg + _buffer_size, _sys_err_msg.begin());
    }
    else {
        _sys_err_msg = std::wstring(L"Unknown error. (FormatMessage failed)");
    }
}

也许这对你们中的一些人会有用。


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