MFCベースのC++アプリで「設定XMLを読み込んで、値を編集して、また保存する」――この一連の処理を、C#のXDocumentのような感覚で実装したい場面はよくあります。本記事では、/clr(C++/CLI)を使わないネイティブC++を前提に、Windows標準のMSXMLを中心に、実務で迷わない選び方と実装例をまとめます。
題材にする設定XML(よくある構成ファイル)
今回のような「属性に数値やフラグを持つ設定XML」は、DOM(ツリー)として扱えるライブラリを選ぶと実装が一気に楽になります。例として、次のようなXMLを読み書きするケースを想定します。
<ROOT>
<SYSTEM lanes="2" isDispense="0" isPack="0" />
<MACHINE address="127.0.0.1" port="6000" identifier="33" />
<CLIENT port="7000" id="34" />
<CUSTOM active="1" />
</ROOT>
結論:MFCネイティブC++での最有力は「MSXML」
結論から言うと、Windowsデスクトップ(MFC)で「追加依存を増やさず、DOM的に読み書きしたい」なら、Microsoft純正のMSXML(COMベース)が最も現実的です。C#のXmlDocumentに近い感覚で扱え、XPathでノードを引けるので、設定ファイル程度なら過不足がありません。
一方で「外部ライブラリOKで、コードをもっと簡潔にしたい」ならpugixml(またはTinyXML-2)が鉄板です。そして「XDocumentと同じAPIが絶対条件」なら/ clrでSystem.Xml.Linq(XDocument)を直接使うのが最短ルートになります。
まず押さえる:XML処理APIの種類(DOMとストリーム)
XMLライブラリは大きく分けて、DOM型(ツリー)と、ストリーム型(前方読み取り)があります。設定ファイルの編集は、基本的にDOM型が向きます。
| 方式 | 特徴 | 向いている用途 | 代表例 |
|---|---|---|---|
| DOM(ツリー) | 読み込んだXMLをメモリ上のツリーとして保持し、ノード/属性を直接編集して保存できる | 設定ファイル、構成ファイル、少量XMLの編集 | MSXML DOM、Windows.Data.Xml.Dom、pugixml、TinyXML-2 |
| ストリーム(前方) | 読み取りは高速・省メモリだが、編集して保存するには別途「書き直し」の設計が必要 | 巨大XMLの逐次処理、ログ解析、変換ツール | XmlLite(IXmlReader/IXmlWriter) |
「読み込み→必要な値だけ取り出す」だけならストリームでも良いのですが、今回のように属性を書き換えて保存するなら、まずDOM型を選ぶのが無難です。
選定の早見表:あなたの条件ならどれが最適か
| 重視すること | おすすめ | 理由 |
|---|---|---|
| Windows標準だけで完結(追加依存なし) | MSXML | Windowsに標準搭載、DOM+XPathで設定XMLに十分。MFCとも相性が良い |
| とにかく実装を簡単に、C#っぽく書きたい(外部ライブラリOK) | pugixml / TinyXML-2 | APIが直感的で短く書ける。設定XML用途での採用例が多い |
| 既にWinRT(C++/WinRT)を使っていて統一したい | Windows.Data.Xml.Dom(C++/WinRT) | Windows APIの延長で扱える。ただしMFCとの混在は注意点あり |
| XDocumentと同じ感覚・同じAPIが絶対条件 | /clr(C++/CLI)でXDocument | 最も「C#そのまま」。ただしネイティブ縛りの要件とは相反する |
Microsoft純正で使えるXMLサポート一覧(MFC視点)
「Microsoft純正でネイティブC++から使えるXML関連」としては、主に次の選択肢があります。
| 名称 | 種類 | 使い勝手 | 備考 |
|---|---|---|---|
| MSXML | DOM/SAX(COM) | DOMなら扱いやすい。XPathも使える | 設定XMLの読み書きに向く。MFCから利用しやすい |
| XmlLite | ストリーム(COM) | 高速・省メモリだが、編集保存は設計が必要 | 巨大XMLや変換処理向け。XDocument的ではない |
| Windows.Data.Xml.Dom | DOM(WinRT) | C++/WinRT経由で書けるが、導入コストは上がる | MFCとの混在でヘッダー競合などの注意点がある |
| .NET System.Xml / System.Xml.Linq | DOM/LINQ to XML(管理) | XDocumentが使える | /clr(C++/CLI)が必要。純ネイティブではない |
MSXMLでXMLを読み書きする(ネイティブC+++MFCの定番)
ここからは、最も採用しやすいMSXML(DOM)で、設定XMLを読み書きする実装例を紹介します。ポイントは次の3つです。
- COM初期化(CoInitialize/CoUninitialize)をスレッドごとに正しく行う
- DOMDocumentはDOMDocument60(MSXML6)を使う(古いバージョン固定は避ける)
- 設定XML用途なら、検証や外部参照は基本的に無効化して堅牢性を上げる
準備:#importでMSXMLのスマートポインタを使う
Visual StudioのC++では、MSXMLをCOMとして扱うのが一般的です。以下は「#importで型ライブラリを読み込む」スタイルの例です。
// 例:プリコンパイルヘッダー(stdafx.h など)か、専用cppで一度だけimport
// <msxml6.dll> は環境のパス解決に依存するので、必要に応じてフルパスや msxml6.dll を指定します。
#import <msxml6.dll> rename_namespace("MSXML") named_guids
// 以降、MSXML::IXMLDOMDocument2Ptr のような型が使える
MFCアプリはUnicode(UTF-16)前提のことが多いので、ファイルパスや文字列は基本的にワイド(wchar_t)で扱うと事故が減ります。
COM初期化をRAIIで包む(MFCアプリでの安定化)
COMは「使うスレッドで初期化→終了時に解除」が原則です。UIスレッドだけで完結するなら起動時に一度だけでも動きますが、将来ワーカースレッドで読み書きする可能性があるなら、RAIIで守っておくのが安全です。
struct CoInitScope
{
HRESULT hr;
CoInitScope()
: hr(::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED)) {}
~CoInitScope()
{
if (SUCCEEDED(hr)) ::CoUninitialize();
}
bool ok() const { return SUCCEEDED(hr); }
};
読み込み:ノードを探して属性を取り出す(XPathで素直に)
MSXMLのDOMは、XPathでノードを検索できます。設定XMLでは「特定の要素の属性を読む」が中心なので、XPathが非常に相性良いです。
#include <atlbase.h> // CComVariant を使うなら
#include <string>
bool LoadConfigMsxml(const wchar_t* filePath)
{
CoInitScope com;
if (!com.ok()) return false;
MSXML::IXMLDOMDocument2Ptr doc;
HRESULT hr = doc.CreateInstance(__uuidof(MSXML::DOMDocument60));
if (FAILED(hr) || doc == nullptr) return false;
// 設定XML用途の基本設定(必要に応じて調整)
doc->async = VARIANT_FALSE;
doc->validateOnParse = VARIANT_FALSE;
doc->resolveExternals = VARIANT_FALSE;
doc->setProperty(L"SelectionLanguage", L"XPath");
VARIANT_BOOL loaded = doc->load(_variant_t(filePath));
if (loaded != VARIANT_TRUE)
{
// 解析エラー内容は parseError から取得できる
MSXML::IXMLDOMParseErrorPtr err = doc->parseError;
// err->reason, err->line, err->linepos など
return false;
}
// /ROOT/SYSTEM の要素を取得
MSXML::IXMLDOMNodePtr nodeSystem = doc->selectSingleNode(L"/ROOT/SYSTEM");
if (nodeSystem == nullptr) return false;
MSXML::IXMLDOMElementPtr elemSystem = nodeSystem;
if (elemSystem == nullptr) return false;
// 属性取得(getAttributeはVARIANTで返る)
_variant_t vLanes = elemSystem->getAttribute(L"lanes");
int lanes = 0;
if (vLanes.vt == VT_BSTR && vLanes.bstrVal != nullptr)
lanes = _wtoi(vLanes.bstrVal);
_variant_t vDispense = elemSystem->getAttribute(L"isDispense");
int isDispense = (vDispense.vt == VT_BSTR) ? _wtoi(vDispense.bstrVal) : 0;
// 以降、MACHINE/CLIENT/CUSTOM も同様に読む
return true;
}
設定ファイルでは「属性が存在しない」「値が空」「数値に変換できない」などが必ず起きます。実務では、毎回vt判定を書くより、次のような小さなヘルパー関数を用意すると保守性が上がります。
int GetAttrInt(MSXML::IXMLDOMElementPtr elem, const wchar_t* name, int defVal)
{
if (elem == nullptr) return defVal;
_variant_t v = elem->getAttribute(name);
if (v.vt == VT_BSTR && v.bstrVal != nullptr && v.bstrVal[0] != L'\\0')
return _wtoi(v.bstrVal);
return defVal;
}
_bstr_t GetAttrBstr(MSXML::IXMLDOMElementPtr elem, const wchar_t* name, const wchar_t* defVal)
{
if (elem == nullptr) return _bstr_t(defVal);
_variant_t v = elem->getAttribute(name);
if (v.vt == VT_BSTR && v.bstrVal != nullptr && v.bstrVal[0] != L'\\0')
return _bstr_t(v.bstrVal);
return _bstr_t(defVal);
}
書き込み:属性を更新してsaveする
MSXMLのDOMは、要素の属性を更新して、そのまま保存できます。既存XMLを読み込んで一部だけ更新するケースは、設定ファイルで最も多いパターンです。
bool SaveConfigMsxml(const wchar_t* filePath)
{
CoInitScope com;
if (!com.ok()) return false;
MSXML::IXMLDOMDocument2Ptr doc;
if (FAILED(doc.CreateInstance(__uuidof(MSXML::DOMDocument60))) || doc == nullptr)
return false;
doc->async = VARIANT_FALSE;
doc->validateOnParse = VARIANT_FALSE;
doc->resolveExternals = VARIANT_FALSE;
doc->setProperty(L"SelectionLanguage", L"XPath");
if (doc->load(_variant_t(filePath)) != VARIANT_TRUE)
return false;
MSXML::IXMLDOMElementPtr systemElem = doc->selectSingleNode(L"/ROOT/SYSTEM");
if (systemElem == nullptr) return false;
// 例:lanes を 2 に設定し、フラグも更新
systemElem->setAttribute(L"lanes", _variant_t(2));
systemElem->setAttribute(L"isDispense", _variant_t(0));
systemElem->setAttribute(L"isPack", _variant_t(0));
// MACHINE の address を更新
MSXML::IXMLDOMElementPtr machineElem = doc->selectSingleNode(L"/ROOT/MACHINE");
if (machineElem != nullptr)
{
machineElem->setAttribute(L"address", _variant_t(L"127.0.0.1"));
machineElem->setAttribute(L"port", _variant_t(6000));
machineElem->setAttribute(L"identifier", _variant_t(33));
}
// 保存(実務では「一時ファイルに保存→置換」で安全性を上げるのがおすすめ)
if (doc->save(_variant_t(filePath)) != S_OK)
return false;
return true;
}
上の例では「必ず存在する前提」でselectSingleNodeしていますが、設定ファイルは破損や初回起動を考慮し、無ければ生成できるようにしておくと運用が安定します。
初回起動向け:XMLが無い/壊れている場合に新規生成する
「読み込みに失敗したらデフォルト設定で新規生成」まで作っておくと、現場での問い合わせが減ります。MSXMLで新規ドキュメントを作る場合は、ルート要素を作ってappendChildします。
MSXML::IXMLDOMDocument2Ptr CreateDefaultDoc()
{
MSXML::IXMLDOMDocument2Ptr doc;
if (FAILED(doc.CreateInstance(__uuidof(MSXML::DOMDocument60))) || doc == nullptr)
return nullptr;
doc->async = VARIANT_FALSE;
doc->validateOnParse = VARIANT_FALSE;
doc->resolveExternals = VARIANT_FALSE;
// <ROOT> を作る
MSXML::IXMLDOMElementPtr root = doc->createElement(L"ROOT");
doc->appendChild(root);
// <SYSTEM />
MSXML::IXMLDOMElementPtr system = doc->createElement(L"SYSTEM");
system->setAttribute(L"lanes", _variant_t(2));
system->setAttribute(L"isDispense", _variant_t(0));
system->setAttribute(L"isPack", _variant_t(0));
root->appendChild(system);
// <MACHINE />
MSXML::IXMLDOMElementPtr machine = doc->createElement(L"MACHINE");
machine->setAttribute(L"address", _variant_t(L"127.0.0.1"));
machine->setAttribute(L"port", _variant_t(6000));
machine->setAttribute(L"identifier", _variant_t(33));
root->appendChild(machine);
// <CLIENT />
MSXML::IXMLDOMElementPtr client = doc->createElement(L"CLIENT");
client->setAttribute(L"port", _variant_t(7000));
client->setAttribute(L"id", _variant_t(34));
root->appendChild(client);
// <CUSTOM />
MSXML::IXMLDOMElementPtr custom = doc->createElement(L"CUSTOM");
custom->setAttribute(L"active", _variant_t(1));
root->appendChild(custom);
return doc;
}
MSXMLでハマりやすいポイント(MFCアプリでの実務あるある)
| ハマりどころ | 症状 | 対策 |
|---|---|---|
| COM初期化忘れ | CreateInstanceやloadが失敗する、環境によって不安定 | CoInitializeExをRAIIで必ず実行(スレッドごと) |
| XPathが効かない | selectSingleNodeが常にnull | setProperty(“SelectionLanguage”,”XPath”)を設定 |
| 属性取得が面倒 | VARIANT判定や型変換が散らばる | GetAttrInt/GetAttrStringなどのヘルパーを作り、呼び出し側を簡潔に |
| 文字コード/保存形式 | 保存したXMLがUTF-16になり、他ツールで扱いづらい | 運用でUTF-8が必要なら、charset指定やテンプレートXMLを用意し、保存結果を確認する |
| 安全性(外部参照) | 外部エンティティ等が絡むと想定外の読み込みが起きる可能性 | resolveExternalsをfalse、検証をオフ。利用環境に応じてDTD禁止設定も検討 |
Windows.Data.Xml.Dom(WinRT)+C++/WinRTを使う選択肢
Microsoft純正としてもう一つ挙げられるのが、WinRTのWindows.Data.Xml.Domです。内部的にはMSXMLのラッパー的な位置づけで、C++/WinRT経由でDOM操作ができます。
ただしMFCアプリにそのまま持ち込むと、プロジェクト設定やヘッダーの依存関係で引っかかることがあります。特に次のような環境では注意が必要です。
- C++/WinRT有効化により、MIDL設定やビルド構成が変わり、独自COMコンポーネントを持つプロジェクトに影響する可能性
- 一部のWinRTヘッダー(例:Windows.Globalization を間接的に含むもの)と、MFCヘッダーでコンパイルエラーが出るケース
現場では、XML処理だけ別DLL(WinRTを使う側)に切り出し、MFC本体からはDLL越しに呼び出す構成にして衝突を回避することが多いです。
(参考)C++/WinRTでDOM的に属性を読むイメージ
ファイル入出力はMFC側で行い、「テキストとして読み込んでLoadXmlする」方式にすると、WinRTのStorageFile依存を避けやすくなります。
#include <winrt/Windows.Data.Xml.Dom.h>
#include <winrt/base.h>
#include <string>
bool ParseWithWinRT(const std::wstring& xmlText)
{
winrt::init_apartment(winrt::apartment_type::single_threaded);
winrt::Windows::Data::Xml::Dom::XmlDocument doc;
doc.LoadXml(xmlText);
auto systemNode = doc.SelectSingleNode(L"/ROOT/SYSTEM");
if (!systemNode) return false;
auto systemElem = systemNode.as();
int lanes = _wtoi(systemElem.GetAttribute(L"lanes").c_str());
// 更新も可能
systemElem.SetAttribute(L"lanes", L"2");
return true;
}
この方法は「すでにC++/WinRTを使っている」「WinRT側で統一したい」場合に検討価値があります。逆に、XMLだけのために導入するなら、MSXML(COM)の方が圧倒的に手堅いです。
/clr(C++/CLI)でXDocumentを使う方法(最もXDocumentに近い)
質問で「C#のXDocumentのような感覚で」と言われた場合、APIだけで言えばSystem.Xml.LinqのXDocumentをそのまま使うのが最短です。C++/CLIを許容できるなら、次のようにC#とほぼ同じ書き味になります。
// /clr を有効にし、System.Xml と System.Xml.Linq を参照に追加する前提
#using <System.Xml.dll>
#using <System.Xml.Linq.dll>
using namespace System;
using namespace System::Xml::Linq;
void LoadAndSaveWithXDocument(String^ path)
{
XDocument^ doc = XDocument::Load(path);
XElement^ root = doc->Element("ROOT");
XElement^ system = root->Element("SYSTEM");
int lanes = (int)system->Attribute("lanes");
system->SetAttributeValue("lanes", 2);
doc->Save(path);
}
ただし、純ネイティブにこだわるプロジェクトでは、/clrが入るだけでビルドやデバッグ、配布形態が変わります。要件に「ネイティブC++のみ」とあるなら、候補としては理解しておくが採用は慎重に、という位置づけが現実的です。
サードパーティXMLライブラリ(実務でよく使われる軽量DOM)
「Windows標準にこだわらない」「依存を増やしてもよい」場合、実務では軽量XMLライブラリが非常に強い選択肢になります。とくに設定ファイル用途で名前が挙がりやすいのが、pugixml / TinyXML-2 / RapidXMLです。
| ライブラリ | 特徴 | 向いているケース | 注意点 |
|---|---|---|---|
| pugixml | APIが直感的、XPath対応、機能と軽さのバランスが良い | 設定XMLの読み書き、保守しやすい実装にしたい | 導入時にライセンス確認と、文字コード方針(UTF-8運用など)を決める |
| TinyXML-2 | 非常に軽量、学習コストが低い | 小規模設定、XPathが不要でとにかく軽く | XPath非対応(基本は手で辿る)。UTF-8前提の運用が多い |
| RapidXML | 高速、ヘッダオンリー | 読み込み速度最優先、バッファ管理も自前で良い | 入力バッファを破壊的に使う設計が多く、保存処理は手作業になりがち |
pugixmlでの読み書き例(設定XMLならこの短さが魅力)
pugixmlは「load_file→child→attribute」の流れが非常に分かりやすく、設定XMLと相性が良いです。XDocumentの“感覚”に近いのは、実務ではむしろpugixmlの方だと感じる場面もあります。
#include "pugixml.hpp"
struct Config
{
int lanes = 2;
int isDispense = 0;
int isPack = 0;
std::string address = "127.0.0.1";
int machinePort = 6000;
int identifier = 33;
int clientPort = 7000;
int clientId = 34;
int customActive = 1;
};
bool LoadConfigPugi(const char* path, Config& cfg)
{
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(path);
if (!result) return false;
pugi::xml_node root = doc.child("ROOT");
pugi::xml_node sys = root.child("SYSTEM");
cfg.lanes = sys.attribute("lanes").as_int(cfg.lanes);
cfg.isDispense = sys.attribute("isDispense").as_int(cfg.isDispense);
cfg.isPack = sys.attribute("isPack").as_int(cfg.isPack);
pugi::xml_node machine = root.child("MACHINE");
cfg.address = machine.attribute("address").as_string(cfg.address.c_str());
cfg.machinePort = machine.attribute("port").as_int(cfg.machinePort);
cfg.identifier = machine.attribute("identifier").as_int(cfg.identifier);
pugi::xml_node client = root.child("CLIENT");
cfg.clientPort = client.attribute("port").as_int(cfg.clientPort);
cfg.clientId = client.attribute("id").as_int(cfg.clientId);
pugi::xml_node custom = root.child("CUSTOM");
cfg.customActive = custom.attribute("active").as_int(cfg.customActive);
return true;
}
bool SaveConfigPugi(const char* path, const Config& cfg)
{
pugi::xml_document doc;
pugi::xml_node root = doc.append_child("ROOT");
auto sys = root.append_child("SYSTEM");
sys.append_attribute("lanes") = cfg.lanes;
sys.append_attribute("isDispense") = cfg.isDispense;
sys.append_attribute("isPack") = cfg.isPack;
auto machine = root.append_child("MACHINE");
machine.append_attribute("address") = cfg.address.c_str();
machine.append_attribute("port") = cfg.machinePort;
machine.append_attribute("identifier") = cfg.identifier;
auto client = root.append_child("CLIENT");
client.append_attribute("port") = cfg.clientPort;
client.append_attribute("id") = cfg.clientId;
auto custom = root.append_child("CUSTOM");
custom.append_attribute("active") = cfg.customActive;
// インデント付きで保存(運用で差分が見やすい)
return doc.save_file(path, " ");
}
このように、設定値を構造体にマッピングしてしまえば、XMLライブラリが何であっても「アプリ側のロジック」がスッキリします。実務ではここが最重要ポイントです。
実務でおすすめする設計:XML処理とビジネスロジックを分離する
設定XMLの読み書きは、最初は手続き的に書いても動きます。しかし、設定項目が増えたり、バージョンアップで要素が増えると、すぐにメンテナンスが苦しくなります。そこでおすすめなのが、次の分離です。
- Config構造体(純データ):アプリが必要とする値を型付きで保持
- ConfigXml(入出力専用):XML読み込み・書き込みだけを担当
- アプリ本体:Config構造体だけを見て動く(XMLの存在を忘れる)
分離すると、将来「XML→JSONに変えたい」「INIに戻したい」となっても、影響範囲を最小化できます。
設定のバージョニング(地味に効く運用テク)
設定ファイルは運用中に肥大化しがちです。後で詰まらないために、ルート要素にバージョン属性を付けておくのが定番です。
<ROOT version="1">
...
</ROOT>
読み込み時にversionを見て、足りない要素はデフォルト補完、不要になった要素は無視、といった戦略が取りやすくなります。
保存は「一時ファイル→置換」で壊れにくくする
設定ファイル保存中に電源断や強制終了が起きると、XMLが途中までしか書けず破損します。対策として、次の流れを推奨します。
- config.xml.tmp など一時ファイルにsave
- save成功を確認
- 既存config.xmlをバックアップし、tmpをリネームして置換
WindowsならMoveFileEx(MOVEFILE_REPLACE_EXISTING)などで置換の原子性を高められます。XMLライブラリ選定より、こうした運用面の堅牢化がトラブル削減に直結します。
よくある質問:結局、C++エキスパートは何を使っているのか?
「C++の現場ではどれがよく使われるのか」は、プロダクトの性質で分かれます。体感としては次の傾向です。
| 現場の条件 | よくある選択 | 理由 |
|---|---|---|
| Windows専用、長期運用、標準機能で完結させたい | MSXML | 追加依存が無く、古いMFC資産でも動く。監査や配布で説明しやすい |
| クロスプラットフォームも視野、またはコードの簡潔さ重視 | pugixml / TinyXML-2 | 導入が簡単で、DOM操作が短く書ける。ユニットテストもしやすい |
| 既に.NET資産が多い、XDocumentを最大限活かしたい | /clr + XDocument | APIが強力で開発が速い。ただしネイティブ縛りがあると難しい |
まとめ:迷ったらMSXML、簡潔さならpugixml、完全一致ならXDocument
- ネイティブC+++MFCで、Windows標準に寄せたいならMSXMLが第一候補
- 追加依存を許容でき、設定XMLを短く保守しやすく書きたいならpugixmlが強い
- どうしてもC#のXDocumentそのままの体験が必要なら、プロジェクト条件次第で/clrを検討
そして、どのライブラリでも効く“勝ち筋”は、XMLを直接触るコードをアプリの奥まで広げず、構造体にマッピングして入出力層に閉じ込めることです。ここまで整えておけば、将来の仕様追加やリファクタリングのコストが段違いに下がります。

コメント