WindowsのウィンドウプロシージャでC++例外をstd::exception_ptrで扱うお話

 Windowsのウィンドウ周りのプログラミングする時に必ず出てくるウィンドウプロシージャですが、ウィンドウプロシージャの外にそのまま例外を投げようとすると、握りつぶされたりプロセスを強制終了されたりする事が(en-usのほうの)MSDNのWindowProc callback functionのRemarksに書かれています。

WindowProc callback function
https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573.aspx

そこで、C++11から追加されたstd::exception_ptrにウィンドウプロシージャの例外を一旦格納して、メッセージループで投げ直すことで問題なく例外処理をできるようにします。
 それでは、std::exception_ptrを使ったウィンドウプロシージャとメッセージループを以下に示します。

ウィンドウプロシージャ

 ウィンドウプロシージャ内で発生した全ての例外をstd::current_exceptionを使ってstd::exception_ptrに一旦格納して、ウィンドウプロシージャからそのまま例外が外に出ないようにします。

// ウィンドウプロシージャとメッセージループがある関数から見えるように
// とりあえずグローバル変数にしているproc_except
std::exception_ptr proc_except;

LRESULT CALLBACK procedure(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) noexcept
{
    try {
        if( msg == WM_DESTROY ) {
            PostQuitMessage( 0 );
            return 0;
        }

        // ここで例外を投げてみるテスト的な
        throw std::runtime_error( "error" );
    }
    catch( ... ) {
        // 例外全部受け取って、例外をexception_ptrに格納する
        proc_except = std::current_exception();
        // とりあえずプロシージャから抜ける
        return 0;
    }

    return DefWindowProcW( hwnd, msg, wparam, lparam );
}

メッセージループ

 ウィンドウプロシージャで投げた例外をメッセージループ内で受け取り、std::exception_ptrに例外が格納されている場合、std::rethrow_exceptionでメッセージループで例外を投げ直します。あとは通常の例外処理になりtry-catchで処理できます。

// グローバル変数
// std::exception_ptr proc_except;

MSG msg;
for(;;) {
    auto const ret = GetMessageW( &msg, nullptr, 0, 0 );
    if( ret == 0 || ret == -1 ) {
        break;
    }

    DispatchMessageW( &msg );


    // プロシージャで発生した例外があれば投げ直す
    if( proc_except ) {
        std::rethrow_exception( proc_except );
    }
}

WM_CREATEで例外を投げる場合

 WM_CREATEで例外を投げる場合は、CreateWindowExやCreateWindowの直後にstd::exception_ptrに例外が格納されているかどうかを調べてstd::rethrow_exceptionを呼び出します。また、CreateWindowEx自体が失敗することを考えてstd::exception_ptrをチェックすることとは別に返り値をチェックしたほうがいいでしょう。

HWND hwnd = CreateWindowExW( 
    0, wc.lpszClassName, L"window", WS_OVERLAPPEDWINDOW,
    0, 0, 1024, 769, nullptr, nullptr, wc.hInstance, nullptr
);
// プロシージャで投げた例外はこっち
if( proc_except ) {
    std::rethrow_exception( proc_except );
}
// CreateWindowEx自体が失敗した場合はNULLが返るので
if( !hwnd ) {
    throw std::runtime_error( "CreateWindowEx failed" );
}

 ちなみに、ウィンドウプロシージャのWM_CREATEで例外を発生させた時、catch内で-1を返すことを考えてstd::exception_ptr、hwndのNULLチェックの順番にしています。これはWM_CREATEの処理で-1を返すとCreateWindowExがNULLを返すようになるためです。

参考

WindowProc callback function
https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573.aspx

exception - cpprefjp
https://cpprefjp.github.io/reference/exception.html

Error handling - cppreference.com
http://en.cppreference.com/w/cpp/error

WM_CREATE message
https://msdn.microsoft.com/en-us/library/windows/desktop/ms632619(v=vs.85).aspx

コード

メッセージループでstd::runtime_errorが発生してerrorをエラー出力するようになっています。

#include <windows.h>
#include <exception>
#include <iostream>

std::exception_ptr proc_except;

LRESULT CALLBACK procedure(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) noexcept
{
    try {
        if( msg == WM_DESTROY ) {
            PostQuitMessage( 0 );
            return 0;
        }

        // ここで例外を投げてみる
        throw std::runtime_error( "error" );
    }
    catch( ... ) {
        // 例外全部受け取って、例外をexception_ptrに保存する
        proc_except = std::current_exception();
        // とりあえずプロシージャから抜ける
        return 0;
    }

    return DefWindowProcW( hwnd, msg, wparam, lparam );
}

int main()
{
    try {
        WNDCLASSEXW const wc = {
            sizeof( WNDCLASSEXW ), CS_VREDRAW | CS_HREDRAW, procedure,
            0, 0, GetModuleHandleW( nullptr ), nullptr, LoadCursorA( nullptr, IDC_ARROW ),
            static_cast< HBRUSH >( GetStockObject( WHITE_BRUSH ) ),
            nullptr, L"nanka_window_class_name", nullptr
        };
        if( !RegisterClassExW( &wc ) ) {
            throw std::runtime_error( "RegisterClassEx failed" );
        }

        HWND hwnd = CreateWindowExW( 
            0, wc.lpszClassName, L"window", WS_OVERLAPPEDWINDOW,
            0, 0, 1024, 769, nullptr, nullptr, wc.hInstance, nullptr
        );
        // プロシージャで投げた例外はこっち
        if( proc_except ) {
            std::rethrow_exception( proc_except );
        }
        // CreateWindowEx自体が失敗した場合はNULLが返るので
        if( !hwnd ) {
            throw std::runtime_error( "CreateWindowEx failed" );
        }

        ShowWindow( hwnd, SW_SHOW );
        UpdateWindow( hwnd );

        MSG msg;
        for(;;) {
            auto const ret = GetMessageW( &msg, nullptr, 0, 0 );
            if( ret == 0 || ret == -1 ) {
                break;
            }

            DispatchMessageW( &msg );

            // プロシージャで発生した例外があれば投げ直す
            if( proc_except ) {
                std::rethrow_exception( proc_except );
            }
        }
    }
    catch( std::exception const& e ) {
        std::cerr << e.what() << std::endl;
    }
    catch( ... ) {
        std::cerr << "unknown error" << std::endl;
    }
}
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.