C++/WinRTでJsonArrayをClearして使い回せない理由と安全なJSON生成方法

C++/WinRT(Windows.Data.Json)でJsonObjectに複数の配列を入れる際、「JsonArrayを1個だけ用意してInsert→Clear→再利用」とすると、なぜか全キーが同じ配列内容になります。本記事では原因(参照共有)を噛み砕いて説明し、実務で迷わない実装例と整理術を紹介します。

目次

結論:JsonArrayは参照型なので、Insertしても「中身は複製されない」

先に結論をはっきりさせると、JsonArrayを1インスタンスだけClearしながら使い回す設計は成立しません。理由はシンプルで、C++/WinRTのWindows::Data::Json::JsonArrayは「値」ではなく参照(ハンドル)として振る舞うWinRTオブジェクトだからです。

JsonObject::Insert()(内部的にはマップに値を入れる操作)は、配列要素を丸ごとコピーして別物として保持するのではなく、そのJsonArrayオブジェクトへの参照を保持します。結果として、同じJsonArrayを何回Insertしても、JsonObjectの複数プロパティが同一オブジェクトを共有します。

やりたいこと期待する動き実際の動き
arrを詰めて「VariableKey」にInsert「VariableKey」だけに配列内容が固定される「VariableKey」はarrの参照を持つ
arr.Clear()して別内容を詰め直す「VariableKey」は変わらない同じarrを共有しているので「VariableKey」も一緒に変わる
「VariableValue」にInsert「VariableValue」は新しい配列、Keyは古い配列両方とも同じarrを参照し、最後に詰めた内容で揃う

現象を最小コードで再現する(「最後の配列に揃う」問題)

まずは、質問内容の状況をできるだけ短いコードで再現します。ポイントは、同じJsonArray変数をInsert→Clear→Insertしていることです。

#include <winrt/Windows.Data.Json.h>
#include <iostream>

using namespace winrt;
using namespace Windows::Data::Json;

int main()
{
    init_apartment();

    JsonObject finalOutput;
    JsonArray arrOutput;

    // 1) "VariableKey" を入れる
    arrOutput.Append(JsonValue::CreateStringValue(L"KeyA"));
    arrOutput.Append(JsonValue::CreateStringValue(L"KeyB"));
    finalOutput.Insert(L"VariableKey", arrOutput);

    // 2) 使い回すつもりでクリア
    arrOutput.Clear();

    // 3) "VariableValue" を入れる
    arrOutput.Append(JsonValue::CreateStringValue(L"Val1"));
    arrOutput.Append(JsonValue::CreateStringValue(L"Val2"));
    finalOutput.Insert(L"VariableValue", arrOutput);

    std::wcout << finalOutput.Stringify().c_str() << std::endl;
}

期待としては、次のようなJSONを想像するはずです。

{
  "VariableKey": ["KeyA", "KeyB"],
  "VariableValue": ["Val1", "Val2"]
}

しかし実際には、両方が最後に詰めた配列(Val1/Val2)になりがちです。つまり次のようになります。

{
  "VariableKey": ["Val1", "Val2"],
  "VariableValue": ["Val1", "Val2"]
}

これはバグでも「Insertが壊れている」わけでもなく、参照共有の仕様どおりの挙動です。

なぜこうなるのか:C++/WinRTの「オブジェクトは軽いハンドル」問題

C++/WinRTの型(JsonArrayやJsonObjectなど)は、C++の見た目では値型っぽく見えますが、実態はWinRTランタイムクラス(COMベース)へのスマート参照です。

そのため、次のような操作は「深いコピー」ではなく、同じ実体を指すハンドルのコピーになります。

  • JsonArray a = b;(代入)
  • finalOutput.Insert(..., a);(格納)
  • 関数引数や戻り値で渡す(多くの場合、参照カウントが増えるだけ)

そして同じ実体を共有している以上、a.Clear()やa.Append()といった「実体の変更」は、共有しているすべての参照に反映されます。これは、C#で同じList<T>インスタンスを複数のプロパティに入れてから、中身をクリアして詰め直すと全部同じになるのと、発想としては近いです。

Insertが保持しているのは「JsonArrayそのもの」ではなく「IJsonValue」

JsonObjectが持つ値は、概念的にはhstring -> IJsonValueのマップです。JsonArrayもIJsonValueを実装するオブジェクトなので、Insertでは「IJsonValueとしての参照」が格納されます。

ここで重要なのが、IJsonValueは参照型であり、Insertはディープコピーをしないという点です。したがって、同一インスタンスの使い回し=同一参照の使い回しになります。

正しい実装方針:配列プロパティごとに新しいJsonArrayを作る

解決策は「配列の数だけJsonArrayインスタンスを用意する」です。ただし、変数名を10個用意する必要はありません。必要なのは「インスタンス(実体)が別であること」であって、「ローカル変数が10個あること」ではないからです。

最も分かりやすい書き方(素直に別変数を持つ)

JsonObject finalOutput;

// "VariableKey" 用
JsonArray arrKey;
for (auto const& key : VariableKeyArray) {
arrKey.Append(JsonValue::CreateStringValue(key));
}
finalOutput.Insert(L"VariableKey", arrKey);

// "VariableValue" 用
JsonArray arrValue;
for (auto const& value : VariableValueArray) {
arrValue.Append(JsonValue::CreateStringValue(value));
}
finalOutput.Insert(L"VariableValue", arrValue);

この形なら、Insert後にarrKeyを触ってもarrValueは影響を受けないため、直感どおりのJSONが作れます。

変数名を増やしたくない場合:スコープを切って“同名変数”を使う

「配列は10個必要だが、ローカル変数が増えすぎて見通しが悪い」という場合は、ブロックでスコープを切って同じ変数名を再利用できます。重要なのは、ブロックごとに新しいJsonArrayインスタンスを生成してInsertすることです。

JsonObject finalOutput;

{
    JsonArray arr;
    for (auto const& key : VariableKeyArray) {
        arr.Append(JsonValue::CreateStringValue(key));
    }
    finalOutput.Insert(L"VariableKey", arr);
}

{
    JsonArray arr;
    for (auto const& value : VariableValueArray) {
        arr.Append(JsonValue::CreateStringValue(value));
    }
    finalOutput.Insert(L"VariableValue", arr);
}

この書き方だと、ローカル変数はいつもarrの1個だけに見えますが、Insertされる配列実体は別物なので安全です。Insertされた後はJsonObject側が参照を保持するため、ブロックを抜けてローカル変数が消えても、配列は生存します。

より実務向け:共通関数(ファクトリ)で重複を減らす

配列の型が「文字列配列」に揃っているなら、ヘルパー関数を1つ用意すると一気にスッキリします。質問のコードに最も近い形で、読みやすさと安全性を両立できます。

JsonArray MakeStringArray(std::vector<winrt::hstring> const& src)
{
    JsonArray arr;
    for (auto const& s : src) {
        arr.Append(JsonValue::CreateStringValue(s));
    }
    return arr; // 返しても参照カウントが引き継がれるだけで軽い
}

JsonObject finalOutput;
finalOutput.Insert(L"VariableKey",   MakeStringArray(VariableKeyArray));
finalOutput.Insert(L"VariableValue", MakeStringArray(VariableValueArray));

戻り値で返したJsonArrayは「値」として見えますが、実態はハンドルなのでコピーコストは小さく、さらにInsert時に別インスタンスとして保持されます。Clearで使い回す必要がそもそもなくなるのが大きなメリットです。

「Clearして再利用」は不可でも、「変数は1つ」で運用できる

質問では「配列インスタンスを1個にしたい」という意図が強いですが、実務で本当に欲しいのは多くの場合、次のどちらかです。

  • コードを短くしたい(変数名やループの重複を減らしたい)
  • メモリや生成コストを抑えたい(インスタンス数を減らしたい)

前者(可読性)については、上で紹介したファクトリ関数やラムダで解決できます。後者(コスト)については、WinRTのJsonArrayを10個作ること自体がボトルネックになるケースは多くありません。むしろ、誤って同一参照を共有してデータが壊れるほうが、デバッグコストとして高く付きます。

それでも「変数としては1つにしたい」場合は、次のようにClearではなく“新規生成で差し替える”という手があります。これならローカル変数は1つのまま、Insertされる配列実体は毎回別になります。

JsonObject finalOutput;
JsonArray arr; // 変数は1つ

// 1つ目
arr = JsonArray(); // 新規インスタンス
for (auto const& key : VariableKeyArray) {
    arr.Append(JsonValue::CreateStringValue(key));
}
finalOutput.Insert(L"VariableKey", arr);

// 2つ目
arr = JsonArray(); // ここが重要:Clearではない
for (auto const& value : VariableValueArray) {
    arr.Append(JsonValue::CreateStringValue(value));
}
finalOutput.Insert(L"VariableValue", arr);

ここではarrという“変数名”は同じでも、代入で別のJsonArray実体を作っています。これが「使い回し」ではなく「作り直し」です。Clearは同じ実体を消すのでダメ、新規生成は別実体なのでOK、と覚えると迷いません。

どうしても「コピーした配列」を作りたいときの選択肢(ディープコピー)

通常は「必要な分だけJsonArrayを新規作成」で十分ですが、たとえば次のような要件があると「複製」が欲しくなることがあります。

  • 共通の雛形配列をベースに、少しだけ要素を変えた配列を複数作りたい
  • Insert済みの配列に影響しない形で、後から加工用の配列を作り直したい

この場合、“参照をコピー”ではなく“内容をコピー(ディープコピー)”が必要です。Windows.Data.Jsonには「Clone」系のメソッドが分かりやすく用意されていないため、実装方針としては大きく2つあります。

方法コピーの深さメリットデメリットおすすめ場面
Stringify→Parseディープコピー実装が短い/ネストが深くてもOK文字列化のコストが増える配列サイズが小さめ、まずは確実に動かしたい
手動で再帰コピーディープコピー不要な文字列化がない/高速化余地がある実装量が増える/型分岐が必要大量データで性能が問題になった、制御したい

手軽:Stringify→Parseでディープコピーする

最短で安全なのは、いったんJSON文字列にしてからParseし直す方法です。中間文字列が発生しますが、参照共有は確実に断ち切れます。

JsonArray DeepCopy(JsonArray const& src)
{
    // srcの内容を文字列化 → それをParseして新しい配列を生成
    return JsonArray::Parse(src.Stringify());
}

「雛形arrTemplateを少し編集して別プロパティへ」という場合でも、Insert前にこのDeepCopyを挟めば、後から雛形を変えても既存の出力は壊れません。

性能重視:IJsonValueを見て再帰的に作り直す

ネストしたオブジェクトや配列まで含めて厳密にコピーしたい場合、IJsonValue::ValueType(ValueType列挙)で分岐しながら再帰的に構築するのが王道です。実装は長くなりますが、「どこで何をコピーしたか」が明確になります。

IJsonValue DeepCopyValue(IJsonValue const& v);

JsonArray DeepCopyArray(JsonArray const& src)
{
    JsonArray dst;
    for (IJsonValue const& item : src) {
        dst.Append(DeepCopyValue(item));
    }
    return dst;
}

JsonObject DeepCopyObject(JsonObject const& src)
{
    JsonObject dst;
    for (auto const& kv : src) { // kv : IKeyValuePair<hstring, IJsonValue>
        dst.Insert(kv.Key(), DeepCopyValue(kv.Value()));
    }
    return dst;
}

IJsonValue DeepCopyValue(IJsonValue const& v)
{
    switch (v.ValueType()) {
    case JsonValueType::Array:
        return DeepCopyArray(v.GetArray());
    case JsonValueType::Object:
        return DeepCopyObject(v.GetObject());
    case JsonValueType::String:
        return JsonValue::CreateStringValue(v.GetString());
    case JsonValueType::Number:
        return JsonValue::CreateNumberValue(v.GetNumber());
    case JsonValueType::Boolean:
        return JsonValue::CreateBooleanValue(v.GetBoolean());
    case JsonValueType::Null:
    default:
        return JsonValue::CreateNullValue();
    }
}

「そこまでやる必要があるか?」はデータ量と要件次第ですが、参照共有が原因で起きる事故を完全に防げるのは大きいです。

実務でハマりやすいポイントとチェック方法

Insertの“前”に最終形を作ってしまう

参照共有が怖い場合は、単純に「Insertする前に配列を完成させる」設計に寄せるだけで事故が減ります。つまり、Insert後に配列へAppendしないという運用ルールです。

  • OK:配列を全部AppendしてからInsertする
  • NG:InsertしてからAppendやClearで内容をいじる

同一インスタンスを共有しているか確認する(デバッグ用)

「本当に同じ配列実体を見ているのか?」を確かめたいときは、C++/WinRTのwinrt::get_abiで内部ポインタ(ABIポインタ)を見て比較するのが手っ取り早いです。ここではデバッグ用途としての例を示します。

#include <winrt/base.h>

auto p1 = winrt::get_abi(arrOutput);          // JsonArrayの実体
auto p2 = winrt::get_abi(finalOutput.GetNamedArray(L"VariableKey"));
auto p3 = winrt::get_abi(finalOutput.GetNamedArray(L"VariableValue"));

std::wcout << L"arrOutput    : " << p1 << std::endl;
std::wcout << L"VariableKey  : " << p2 << std::endl;
std::wcout << L"VariableValue: " << p3 << std::endl;

同じ値が出ていれば、それは同一インスタンスを共有しています。「Insertしたから別物になったはず」という思い込みが崩れるので、原因切り分けが早くなります。

実践例:複数配列+補助情報を含むJSONを安全に組み立てる

最後に、実務でよくある「配列が複数あり、さらにメタ情報(タイムスタンプやフラグ)も付く」ケースを例に、安全な書き方をまとめます。ポイントは、各配列はその場で新規生成し、完成してからInsertすることです。

#include <winrt/Windows.Data.Json.h>
#include <chrono>

using namespace winrt;
using namespace Windows::Data::Json;

JsonArray MakeStringArray(std::vector<hstring> const& src)
{
    JsonArray a;
    for (auto const& s : src) {
        a.Append(JsonValue::CreateStringValue(s));
    }
    return a;
}

int main()
{
    init_apartment();

    std::vector<hstring> keys   = { L"KeyA", L"KeyB" };
    std::vector<hstring> values = { L"Val1", L"Val2" };
    std::vector<hstring> notes  = { L"cached", L"validated" };

    JsonObject out;

    out.Insert(L"VariableKey",   MakeStringArray(keys));
    out.Insert(L"VariableValue", MakeStringArray(values));
    out.Insert(L"Notes",         MakeStringArray(notes));

    out.Insert(L"IsSuccess", JsonValue::CreateBooleanValue(true));
    out.Insert(L"Version",   JsonValue::CreateNumberValue(1));

    // 例:ISO8601文字列などを入れたい場合は別途整形
    out.Insert(L"GeneratedAt", JsonValue::CreateStringValue(L"2025-12-12T00:00:00Z"));

    std::wcout << out.Stringify().c_str() << std::endl;
}

この設計であれば、配列の数が増えても「参照共有で壊れる」リスクがなく、後から項目追加しても読みやすさを保ちやすいです。

まとめ:迷ったら「Clearで使い回さない」「新規生成してInsertする」

  • JsonArrayは参照型であり、Insertは内容のディープコピーをしない
  • 同一JsonArrayを複数プロパティにInsertすると、全プロパティが同じ配列実体を共有する
  • その状態でClearやAppendをすると、Insert済みのプロパティもまとめて書き換わる
  • 解決策は、配列ごとに新しいJsonArrayインスタンスを作る(変数名は増やさなくてよい)
  • 雛形を複製したいなら、Stringify→Parseや再帰コピーでディープコピーを作る

WinRTのJSON生成は便利ですが、参照共有の性質を一度踏み外すと「値が勝手に変わる」ように見えて追いかけづらいバグになります。今回のポイントを押さえて、Insertするオブジェクトは“その場で完成させてから渡す”という方針に寄せると、安定した実装になります。

この記事を書いた人

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

コメント

コメントする

目次