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するオブジェクトは“その場で完成させてから渡す”という方針に寄せると、安定した実装になります。

コメント