7

我正在编写一个使用单个对话框的(C++)应用程序。在设置消息泵和处理程序后,我开始想知道如何将 C++ 异常传播到我的原始代码(CreateDialogParam例如,调用 的代码)。

这是我的意思的骨架示例:

BOOL CALLBACK DialogProc(HWND, UINT msg, WPARAM, LPARAM)
{
    if(msg == WM_INITDIALOG) //Or some other message
    {
        /*
            Load some critical resource(s) here. For instnace:

            const HANDLE someResource = LoadImage(...);

            if(someResource == NULL)
            {
            ---> throw std::runtime_error("Exception 1"); <--- The exception handler in WinMain will never see this!
                Maybe PostMessage(MY_CUSTOM_ERROR_MSG)?
            }
        */

        return TRUE;
    }

    return FALSE;
}

//======================

void RunApp()
{
    const HWND dlg = CreateDialog(...); //Using DialogProc

    if(dlg == NULL)
    {
        throw std::runtime_error("Exception 2"); //Ok, WinMain will see this.
    }

    MSG msg = {};
    BOOL result = 0;

    while((result = GetMessage(&msg, ...)) != 0)
    {
        if(result == -1)
        {
            throw std::runtime_error("Exception 3"); //Ok, WinMain will see this.
        }

        //Maybe check msg.message == MY_CUSTOM_ERROR_MSG and throw from here?

        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

//======================

int WINAPI WinMain(...)
{
    try
    {
        RunApp();
        //Some other init routines go here as well.
    }

    catch(const std::exception& e)
    {
        //log the error
        return 1;
    }

    catch(...)
    {
        //log the error
        return 1;
    }

    return 0;
}

如您所见,WinMain将处理“异常 2”和“3”,但处理“异常 1”。

我的基本问题很简单;将这些错误传播到原始“调用”代码的优雅方法是什么?

我想也许可以使用自定义消息并将实际的throw-statements 移到消息泵(in RunApp()),但我不确定这将如何工作,因为我一般对 Windows 的经验相对较少。

也许我对这种情况的看法都错了。当您在消息处理程序中时,当发生致命事件(即,获取关键资源失败,并且没有恢复的机会)时,通常如何摆脱困境?

4

4 回答 4

9

AFAIK WinAPI callbacks (like window/dialog/thread procedures) must not propagate exceptions. This is because WinAPI internals (which call the callback) are not prepared to handle exceptions. They cannot be prepared because exceptions implementation is compiler specific while the code in WinAPI DLLs is fixed so it cannot handle all the possible exceptions propagation implementations.

In some simple cases (especially when you compile with Visual Studio) you may observe that exceptions are propagated as would seem right. This is however a coincidence. And even if your application does not crash you are not sure if the WinAPI functions called in between did not allocate any resources which they did not released due to exception they were not prepared for.

Additional layer of complexity is added by not knowing the source of the callback (in general – for some messages it can be deduced). Messages handled by your dialog procedure go through the message loop if and only if they were posted to your dialog. If they were sent then they skip the loop and are executed directly. Additionally if a message is sent then you don't know who sends the message – is it you? Or maybe Windows? Or some other window in other process trying to do something? (However this would be risky.) You don't know whether the calling site is prepared for an exception.

Some way of workaround is offered by Boost.Exception. The library allows to somehow store the caught exception and use (in particular rethrow) it latter. This way you could wrap your dialog procedure in a throw { ... } catch(...) { ... } where in catch you would capture the exception and store it for latter use.

EDIT: Since C++ 11 the features of Boost.Exception to store an exception object are part of the STD. You may use std::exception_ptr for that.

However some problems still remain. You still have to recover somehow and return some value (for messages that require return value). And you would have to decide what to do with the stored exception. How to access it? Who will do it? What will he/she do with it?

In case of WM_INITDIALOG you may pass arbitrary parameters to the dialog procedure with this message (use appropriate form of dialog creation function) and this could be a pointer to structure which will hold the stored exception (if any). Then you could investigate that structure to see if dialog initialization failed and how. This however cannot be applied simply to any message.

于 2009-07-23T07:24:11.923 回答
1

简而言之,我从不使用异常。然而,有几种方法可以报告任何错误,所有这些方法都以某种形式或形式使用日志记录。

方法 1. 使用 OutputDebugString()。
这很好,因为只有拥有调试器的人才会真正注意到实际上不应该失败的东西。然而,尝试使用异常处理的人显然有很多缺点

方法 2. 使用 MessageBox。
这并不比方法 1 好多少,但是它确实允许非开发人员看到错误。

方法 3. 使用错误记录器。
您可以在失败时添加日志记录并使用标准 Win32 返回码退出应用程序,而不是使用“抛出”然后被捕获然后“记录”:

if(msg == WM_INITDIALOG) //Or some other message
{
    /*
        Load some critical resource(s) here. For instnace:

        const HANDLE someResource = LoadImage(...);

        if(someResource == NULL)
        {
                  LogError("Cannot find resource 'foo');
        }
    */

    return TRUE;
}
于 2009-07-23T01:09:32.010 回答
0

请参阅http://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html

于 2010-01-14T07:46:15.903 回答
0

我会远离注册自定义窗口消息以进行错误处理。我的意思是这种方法可以正常工作,但实际上没有必要。

顺便说一句,您上面的 catch 处理程序应该捕获所有 3 个异常。您的对话过程在调用 CreateDialog 的同一线程上运行。创建无模式对话框不会产生工作线程。无模式对话框仍然通过您的 GetMessage/Translate/Dispatch 循环获取其消息。那里有一个堆栈帧,这意味着当你抛出时,它应该一直展开到你的 WinMain try/catch 块。

这不是您看到的行为吗?

于 2009-07-23T01:09:15.177 回答