C++/WinRTでのwinrt::hresult_error例外処理とWindows.Data.Jsonの安全な扱い方

C++/WinRT で Windows.Data.Json を扱っていると、ちょっとしたキーのタイプミスや JSON の欠損で winrt::hresult_error が頻発し、「HRESULT とメッセージをちゃんとログに残したいのに、うまく外側の catch で拾えない……」という状態になりがちです。この記事では、winrt::hresult_error を安全かつ効率良く扱うための例外設計と、Windows.Data.Json を「例外に頼らず」安全に読むための実践的テクニックを整理します。

目次

C++/WinRT における winrt::hresult_error の基本

まず前提として、C++/WinRT の多くの API はエラー時に HRESULT を返すのではなく、winrt::hresult_error という例外を投げます。内部的には従来の HRESULT が使われていますが、C++ 的には「例外オブジェクト」として扱うのがポイントです。

winrt::hresult_error の代表的なメンバーは次のとおりです。

メンバー型内容
code()winrt::hresult32bit の HRESULT 値。static_cast<uint32_t>() で数値化してログに出すのが一般的。
message()winrt::hstring人間向けのエラーメッセージ(通常はローカライズ済み)。

Windows.Data.Json でキーが存在しない / 型が違うなどの状況になると、この winrt::hresult_error が送出されます。ここでありがちな「悪手」が、内側の try/catch で文字列に変換して投げ直してしまうパターンです。

なぜ「文字列にして投げ直す」と外側で情報が取れなくなるのか

C++ の例外マッチングのルール

C++ の例外は「型」でマッチします。ひとつの try にぶら下がる catch 群のうち、最初に型が合致したものだけが実行され、他の catch にはフォールスルーしません。

&lt;!-- NG 例 --&gt;
try
{
    try
    {
        // Windows.Data.Json の各種アクセス
    }
    catch (const winrt::hresult_error&amp; ex)
    {
        // 文字列に変換してから投げ直してしまう
        auto msg = winrt::to_string(ex.message());
        throw msg; // ここで型は std::string になってしまう
    }
}
catch (const winrt::hresult_error&amp; ex)
{
    // ここには絶対に来ない
}
catch (const std::string&amp; s)
{
    // 内側で投げ直した std::string はここで捕まる
}

ポイントは、

  • 内側 catch で元の winrt::hresult_error は「処理済み」になり、
  • 新たに std::string 型の例外を投げている

という点です。外側の catch (const winrt::hresult_error&) は「winrt::hresult_error 型の例外」しか捕まえないため、型が変わった時点でマッチしません。

つまり「外側で winrt::hresult_error を捕まえたいのに catch(char*) しか動かない」という現象は、内側で「違う型として投げ直している」ことが原因です。

同じ例外を外側へ伝播させる正しいやり方

同じ例外を外側へ伝播させたい場合は、以下のどちらかを使います。

  • catch (...) で何もせず throw; する
  • catch (const winrt::hresult_error&) でログだけ取って throw; する
try
{
    try
    {
        // JSON へのアクセス
    }
    catch (const winrt::hresult_error&amp; ex)
    {
        // ここで軽くログだけ取る
        OutputDebugStringW(ex.message().c_str());

        // 型を変えずに再スロー
        throw; 
    }
}
catch (const winrt::hresult_error&amp; ex)
{
    // ここで HRESULT とメッセージをまとめて処理
}

throw; と書くと「いま捕まえている例外オブジェクト」をそのまま外側に投げ直します。新しい例外オブジェクトを作らないため、外側でも winrt::hresult_error として扱えます。

winrt::hresult_error の型を維持したままメッセージを付け直す

「内側の catch でメッセージだけ整形しておきたい」「HRESULT に対して独自の説明文を付けたい」というケースもあります。その場合も 投げる型は winrt::hresult_error のまま にしておくのがポイントです。

try
{
    // JSON や WinRT API
}
catch (const winrt::hresult_error&amp; ex)
{
    auto hr  = ex.code();
    auto msg = ex.message(); // 元々のメッセージ

    winrt::hstring decorated =
        L&quot;PanelInfos の解析中にエラーが発生しました: &quot; + msg;

    // 型は winrt::hresult_error のまま再スロー
    throw winrt::hresult_error(hr, decorated);
}

こうしておけば、外側では常に catch (const winrt::hresult_error&) で拾えますし、code() から HRESULT、message() から整形済みメッセージを取得できます。

ex.code() と ex.message() の正しい取り出し方

ex.code() は 32bit の HRESULT として扱う

ex.code() は winrt::hresult 型ですが、中身は従来の HRESULT と同じ 32bit 値です。ログやメッセージに出すときは 16 進数で出すのが定番です。

catch (const winrt::hresult_error&amp; ex)
{
    // 32bit 生値を取得
    const auto hr_raw = static_cast&lt;uint32_t&gt;(ex.code());

    // メッセージ (hstring) を std::string (UTF-8) に変換
    const auto u8 = winrt::to_string(ex.message());

    // MFC の CStringW へ
    CStringW wmsg(ex.message().c_str());

    CStringW line;
    line.Format(L&quot;[0x%08X] %s&quot;, hr_raw, (LPCWSTR)wmsg);

    // ログ・ダイアログ表示など
    OutputDebugStringW(line);
}

static_cast<uint32_t> を挟んでおくと、0x80000000 以上のエラーでも符号付き int にならず安全です。

文字列変換は「ナイーブなナロー化」を避ける

C++/WinRT は標準で winrt::to_string / winrt::to_hstring を提供しているので、これを積極的に使うのが安全です。

やりたいこと推奨 API注意点
hstring -> std::string (UTF-8)winrt::to_string(hstr)文字化け防止。単純な std::string(hstr.begin(), hstr.end()) は禁止。
std::string (UTF-8) -> hstringwinrt::to_hstring(str)UTF-8 前提。Shift-JIS などを直接渡さない。
hstring -> CStringWCStringW(hstr.c_str())MFC を使う場合に便利。

よくある危険な例は次のようなコードです。

// NG: UTF-16 を 1バイトずつ削ってしまう
std::string s(ex.message().begin(), ex.message().end());

これでは UTF-16 を 1バイトずつ切り出すことになり、マルチバイト文字は確実に壊れます。必ず C++/WinRT が提供する変換関数を使いましょう。

「レガシー都合で文字列を投げたい」場合の現実解

古いコードや C スタイルのエラーハンドリングを流用していて、「どうしても char* か std::string の例外に統一したい」という現場事情もあると思います。その場合でも、生の C 文字列を投げるのは避ける べきです。

代わりに、標準の std::system_error / std::runtime_error へブリッジしてしまうと、将来の拡張や統合が楽になります。

#include &lt;system_error&gt;

try
{
    // C++/WinRT の API を呼ぶ
}
catch (const winrt::hresult_error&amp; ex)
{
    const auto hr = static_cast&lt;int&gt;(ex.code());
    const auto u8 = winrt::to_string(ex.message());

    // system_error に乗せ替え
    throw std::system_error(hr, std::system_category(), u8);
}

// どこか上位層
catch (const std::system_error&amp; se)
{
    auto hr  = se.code().value();    // HRESULT 相当
    auto msg = se.what();            // UTF-8 文字列
}
catch (const std::exception&amp; e)
{
    // それ以外の std::exception 系
}

この方式だと、

  • エラーコードは std::error_code として保持され
  • メッセージは what() で取得でき
  • 呼び出し元は std::exception ベースの設計に統一できる

というメリットがあります。C++/WinRT の層では winrt::hresult_error、アプリケーション全体では std::exception という二段構えにしておくと整理しやすくなります。

Windows.Data.Json で「例外を出さずに」安全にアクセスする

そもそも JSON から値を取り出すたびに例外が飛んでくるのは精神衛生上よくありません。Windows.Data.Json は、例外を使わずにキー欠損や型ミスマッチを扱える API を用意しているので、積極的に活用しましょう。

TryParse で JSON 構文エラーを一括検出

void MyTest::SetPanelInfos(const CString&amp; json)
{
    Windows::Data::Json::JsonObject obj;

    // まず JSON として妥当かどうかだけチェック
    if (!Windows::Data::Json::JsonObject::TryParse(
            winrt::to_hstring(json), obj))
    {
        throw std::runtime_error(&quot;Failed to parse JSON.&quot;);
    }

    // ここからは「構文は正しい JSON」として扱える
}

これで「JSON として壊れている」ケースと、「キーが足りない/型が違う」ケースを分離できます。前者は std::runtime_error で一括処理、後者は次の「既定値付きアクセサ」で対処すると整理しやすくなります。

既定値付き GetNamedXxx と HasKey の活用

Windows.Data.Json の JsonObject には、以下のような「既定値付きオーバーロード」があります。

用途API動作
文字列の取り出しGetNamedString(name, defaultValue)キー欠損時に defaultValue を返す。例外を投げない。
数値の取り出しGetNamedNumber(name, defaultValue)同上。戻り値は double。
キーの有無確認HasKey(name)存在を bool で返す。true でも型が違えば GetNamedXxx は例外を投げ得る。

これらを組み合わせると、次のように「例外ゼロ」で値を読み出せます。

void MyTest::SetPanelInfos(const CString&amp; json)
{
    Windows::Data::Json::JsonObject obj;
    if (!Windows::Data::Json::JsonObject::TryParse(
            winrt::to_hstring(json), obj))
    {
        throw std::runtime_error(&quot;Failed to parse JSON.&quot;);
    }

    // 共通のヘルパーを用意するとスッキリする
    auto str = [&amp;](const wchar_t* key) -&gt; winrt::hstring
    {
        return obj.GetNamedString(key, L&quot;&quot;);
    };

    auto num = [&amp;](const wchar_t* key) -&gt; double
    {
        return obj.GetNamedNumber(key, 0.0);
    };

    MessageId      = str(L&quot;MessageId&quot;);
    Station        = str(L&quot;Station&quot;);
    Program        = str(L&quot;Program&quot;);
    Order          = str(L&quot;Order&quot;);
    Referencecode  = str(L&quot;Referencecode&quot;); // ← スペルと空白に注意

    OrderQuantity        = num(L&quot;OrderQuantity&quot;);
    OrderQuantityCurrent = num(L&quot;OrderQuantityCurrent&quot;);
    OrderQuantityGood    = num(L&quot;OrderQuantityGood&quot;);
    OrderQuantityBad     = num(L&quot;OrderQuantityBad&quot;);
}

ここで特に注意したいのが、キー名のタイプミスです。質問文にあった L"Re ferencecode" のようなスペース混入は、そのまま「キーが存在しない」扱いになり、例外の原因になります。
既定値付きオーバーロードを使っておけば、こうしたミスも「空文字になるだけ」で済み、アプリが落ちることはありません。

GetNamedNumber の戻り値は double に注意

GetNamedNumber は double を返します。整数として扱いたい場合は、明示的にキャストや範囲チェックを入れると安全です。

int quantity = static_cast&lt;int&gt;(obj.GetNamedNumber(L&quot;OrderQuantity&quot;, 0.0));

// 必要なら範囲チェック
if (quantity &lt; 0) { quantity = 0; }

「JSON 側の型が実は文字列だった」などの場合は、それでも例外が飛ぶことがあるので、厳密にやるなら GetNamedValue で JsonValueType を確認してからパースする、という手もあります。とはいえ、一般的な業務アプリでは「型が設計通りである」という前提で GetNamedNumber をそのまま使ってしまうケースも多いでしょう。

MFC から C++/WinRT を呼び出している例での実装テンプレート

質問の文脈に近い「MFC のダイアログから C++/WinRT の処理を呼び出す」パターンを、例外処理まで含めたテンプレートとして示します。

void CMFCCopyDataDlg::RequestPanelIn(const CString&amp; data)
{
    try
    {
        // JSON 解析&状態更新
        CurrentPanel.SetPanelInfos(data);
    }
    catch (const winrt::hresult_error&amp; ex)
    {
        const auto hr  = static_cast&lt;uint32_t&gt;(ex.code());
        const auto msg = ex.message(); // hstring

        CStringW line;
        line.Format(L&quot;PanelIn 失敗 [0x%08X] %s&quot;, 
                    hr, msg.c_str());

        // ログ or メッセージボックス
        OutputDebugStringW(line);
        AfxMessageBox(line, MB_ICONERROR);
    }
    catch (const std::exception&amp; ex)
    {
        CStringA a;
        a.Format(&quot;std::exception: %s&quot;, ex.what());

        OutputDebugStringA(a);
        AfxMessageBox(CA2W(a), MB_ICONERROR);
    }
    catch (...)
    {
        AfxMessageBox(L&quot;不明なエラーが発生しました。&quot;, MB_ICONERROR);
    }
}

ポイントをまとめると次の通りです。

  • winrt::hresult_error を最初に捕まえ、HRESULT とメッセージをそのまま表示する
  • JSON 構文エラーなど自前の std::runtime_error は std::exception 系でまとめて受ける
  • catch (...) は「最後の保険」として最下部に 1 つだけ置く

この構造にしておくと、「WinRT 由来の失敗か」「自前ロジックの失敗か」「想定外の失敗か」が UI のメッセージだけで判別しやすくなります。

例外設計:どこで投げて、どこで捕まえるか

C++/WinRT の例外設計で混乱しがちなのは、「どの層で winrt::hresult_error を捕まえて、どの層で std::exception に変換するか」というレイヤー分けです。実務では次のようなイメージで分けると整理しやすくなります。

層責務扱う例外
WinRT ラッパー層Windows API を C++ から使いやすい形に変換winrt::hresult_error をそのまま投げる / ログだけ取って再スロー
アプリケーションサービス層業務ロジック、ドメインロジック必要なら std::system_error やドメイン固有例外へ変換
UI 層 (MFC, WinUI など)ユーザーへのメッセージ表示、リトライ制御std::exception / 想定外例外をまとめて捕捉し、ダイアログ表示など

ルールとしては「なるべく例外の型を途中で変えない」「変えるときは層の境界でまとめて変える」と覚えておくと、デバッグ時に迷子になりにくくなります。

よくある落とし穴とチェックリスト

最後に、C++/WinRT と Windows.Data.Json を組み合わせるときによく踏みがちな地雷と、その対策をチェックリスト風にまとめます。

症状原因対策
外側の catch(winrt::hresult_error) に入らない内側の catch で char* や std::string に変換して投げ直している例外の型を変えずに throw; する / 同じ型で再スローする
日本語エラーメッセージが文字化けするUTF-16 を std::string(h.begin(), h.end()) で無理やりナロー化winrt::to_string / CStringW など正しい変換 API を使う
JSON のキーが見つからず例外が多発キー名のタイプミス(例:Re ferencecode)や欠損データを例外で検出しようとしているGetNamedString(name, default) や HasKey を使って例外に依存しない設計にする
数値の桁がずれる / 小数混入でバグGetNamedNumber が double を返すことを忘れている必要に応じて static_cast<int> や範囲チェックを挟む
CString::Format で意味不明な文字列になるCString s; s.Format("Error:%s", s); のように入力と出力に同じ変数を使っている一時変数を用意し、Format の引数に「まだ書き換えていない文字列」を渡す

これらを一つずつ潰していくだけでも、JSON まわりのクラッシュや謎の文字化けはかなり減らせます。

まとめ

winrt::hresult_error と Windows.Data.Json の組み合わせでつまずきやすいポイントを整理してきました。最後に、実践時の指針をコンパクトにまとめます。

  • 例外の型は変えない: 内側の catch で文字列にして投げ直さず、throw; で外側へそのまま伝播させる。
  • 必要なら同じ型で再スロー: メッセージに情報を付け足したい場合も winrt::hresult_error のまま再スローする。
  • ex.code() は 16 進数で記録: static_cast<uint32_t>() して [0x%08X] の形式でログに出すと後から追跡しやすい。
  • 文字列変換は公式 API を使う: winrt::to_string / winrt::to_hstring / CStringW で文字化けを防ぐ。
  • JSON アクセスは既定値付きオーバーロード: GetNamedString / GetNamedNumber の既定値付き版と TryParse を使い、「例外に頼らない」設計にする。
  • 層ごとに例外型を整理: WinRT ラッパー層では winrt::hresult_error、アプリ全体では std::exception 系へと段階的に変換する。

この方針でコードを組み立て直せば、「外側の catch に入らない」「HRESULT とメッセージがバラバラにしか取れない」といった悩みから解放され、ログもデバッグもしやすい堅牢な実装に近づけます。既存コードの例外処理まわりを見直す際のチェックリストとして、ぜひ活用してみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次