VariantInitとVariantClearでバリアント型を安全に扱う
Windows APIやCOM(Component Object Model)では、文字列、数値、オブジェクト、配列などを一つの変数で扱うためにVARIANT型が使われます。Automation対応APIやOLE関連インターフェイスでは便利な反面、格納された値の型とメモリ所有権を意識しないと、メモリリークや二重解放、アクセス違反につながります。
VARIANTを扱う基本は、使用前にVariantInitで初期化し、不要になった時点でVariantClearを呼び出すことです。Windows APIの仕様を調べる際は、Windows API資料のようなリファレンスも参照しながら、型情報とリソース管理の関係を確認すると理解しやすくなります。
VARIANT型が担う役割
VARIANT構造体は、内部のvtメンバーでデータ型を示し、union領域に実際の値を保持します。整数ならVT_I4、倍精度浮動小数点数ならVT_R8、Unicode文字列ならVT_BSTR、COMオブジェクトならVT_DISPATCHやVT_UNKNOWNというように、VARTYPE定数で解釈方法が決まります。
VARIANT value;
VariantInit(&value);
value.vt = VT_I4;
value.lVal = 123;
上の例では、valueは32ビット整数を格納しています。vtと実データのメンバーが一致していなければ、読み出し側が別の型として解釈してしまうため、単にunionのフィールドへ値を代入するだけでは安全とはいえません。
文字列やCOMインターフェイスを格納する場合は、値そのものではなく、別途確保されたリソースへの参照を持つことがあります。そのため、スコープを抜けるだけでは自動的に解放されず、明示的な後処理が必要です。
VariantInitで使用前の状態を整える
VariantInitはVARIANTをVT_EMPTYの状態に設定します。初期化済みのVARIANTは、まだ有効な文字列やオブジェクトを保持していないことを示すため、後でVariantClearを呼び出しても安全に処理できます。ローカル変数として宣言した直後や、APIへ渡す前には必ず初期化します。
VARIANT argument;
HRESULT hr = S_OK;
VariantInit(&argument);
argument.vt = VT_BSTR;
argument.bstrVal = SysAllocString(L"sample");
if (argument.bstrVal == nullptr) {
hr = E_OUTOFMEMORY;
}
VariantClear(&argument);
VariantInitは、すでに有効な値を持つVARIANTの内容を解放する関数ではありません。使用中のVARIANTへ無条件にVariantInitを実行すると、既存のBSTRやインターフェイス参照を失い、リークを招く可能性があります。再利用する場合は、先にVariantClearを呼び出します。
初期化処理で確認したい点は次のとおりです。
- 宣言直後のVARIANTにVariantInitを実行する
- 初期化前のvtやunionメンバーを読み取らない
- 再利用時はVariantInitではなくVariantClearを先に使う
- APIの成功条件と失敗時の値をドキュメントで確認する
VariantClearで内部リソースを解放する
VariantClearは、VARIANTに格納されている型を調べ、必要なリソースを解放してVT_EMPTYに戻します。BSTRならSysFreeString相当の処理が行われ、VT_DISPATCHやVT_UNKNOWNなら参照カウントが減らされます。SAFEARRAYを保持している場合も、条件に応じて配列の解放が実行されます。
VARIANT result;
VariantInit(&result);
HRESULT hr = SomeComMethod(&result);
if (SUCCEEDED(hr)) {
// result.vtを確認して値を利用する
}
VariantClear(&result);
COMメソッドが返したVARIANTは、成功時だけでなく、途中まで値が設定された失敗時にも後処理が必要になることがあります。関数の終了経路が複数ある場合は、すべての経路でVariantClearへ到達できる構造にすると、解放漏れを防ぎやすくなります。
VariantClearの戻り値も無視しない方が安全です。通常はS_OKが返りますが、ロックされたSAFEARRAYや無効な型情報によって失敗する可能性があります。特に配列を扱うコードでは、配列のロック解除が完了しているかを先に確認します。
後処理で意識するリソースには次のようなものがあります。
- BSTR文字列の確保領域
- IUnknownまたはIDispatchの参照カウント
- SAFEARRAYの配列領域
- VT_BYREFが参照する外部データ
- VariantCopyで複製された内部リソース
VARTYPEと所有権を見分ける
VariantClearを正しく使うには、vtの値だけでなく、データを誰が所有しているかを把握する必要があります。たとえばVT_BSTRではbstrValが指す文字列をVARIANTが所有する形になりますが、VT_BYREF | VT_BSTRでは別の場所にあるBSTRを参照しているだけです。参照型を値型と同じ感覚で扱うと、解放対象を誤ることがあります。
VT_ARRAYフラグが付く場合は、SAFEARRAYへのポインターが格納されます。VT_ARRAY | VT_VARIANTのような型では、配列要素それぞれにもVARIANTの解放処理が関係します。配列を生成するSafeArrayCreateや、コピーを行うVariantCopyとの組み合わせも確認が必要です。
型変換にはVariantChangeTypeやVariantChangeTypeExを使えますが、変換後のVARIANTも独立したリソースを持つ場合があります。変換元と変換先の両方を初期化し、それぞれを個別にVariantClearする設計が基本です。
実装例とエラー処理の考え方
文字列を格納してCOMメソッドへ渡す例では、SysAllocStringの失敗を検出し、処理の終了時に必ずVariantClearを呼びます。呼び出し先が引数を読み取るだけなのか、参照を保持するのかによって、呼び出し後の所有権が変わる点にも注意が必要です。
HRESULT CallWithText(const wchar_t* text)
{
VARIANT param;
VariantInit(¶m);
param.vt = VT_BSTR;
param.bstrVal = SysAllocString(text);
if (param.bstrVal == nullptr) {
return E_OUTOFMEMORY;
}
HRESULT hr = InvokeSomewhere(¶m);
HRESULT clearHr = VariantClear(¶m);
if (FAILED(hr)) {
return hr;
}
return clearHr;
}
実際のCOM呼び出しでは、DISPPARAMSの引数配列、逆順に並べるAutomation引数、EXCEPINFO、戻り値VARIANTなど、複数の構造体を管理することがあります。引数配列を構築したら、呼び出し完了後に各要素を解放し、配列そのもののメモリも確実に片付けます。
ソケット通信や別スレッドからCOM処理を呼び出す場合は、VARIANTの管理に加えて実行環境の初期化も必要です。ソケット作成解説で扱われる通信処理と組み合わせる場合も、受信バッファの寿命とVARIANT内のBSTRの寿命を混同しないことが重要です。
よくある誤りを比較する
典型的な問題は、初期化前のVariantClear、VariantInitによる上書き、vtとunionメンバーの不一致です。これらはコンパイル時に検出されにくく、特定の入力やエラー経路で初めて表面化します。コードレビューでは、生成、使用、解放の順序を追跡すると原因を見つけやすくなります。
| 状況 | 問題になりやすい処理 | 安全な処理 |
|---|---|---|
| 新しいVARIANTを使う | 初期化せずにvtへ代入する | VariantInitを先に呼ぶ |
| 値を入れ替える | VariantInitで既存値を上書きする | VariantClear後に再初期化する |
| BSTRを設定する | 文字列リテラルのアドレスを直接入れる | SysAllocStringなどで所有権を整える |
| COMオブジェクトを扱う | 参照カウントを無視する | AddRefやVariantClearの契約を確認する |
| 関数を抜ける | 成功時だけ解放する | 失敗経路も同じ後処理へ集約する |
マルチスレッド環境では、同じVARIANTを複数スレッドが同時に書き換えないようにします。スレッド作成と終了処理については、スレッド生成の注意点も関係しますが、スレッドを作っただけでVARIANTの共有が安全になるわけではありません。必要ならコピーを作り、各スレッドが自分のVARIANTを所有する設計にします。
VariantCopyは内部リソースを適切に複製するため、単純な構造体代入より安全です。ただし、コピー先も初期化済みである必要があり、コピー元とコピー先の両方をVariantClearする責任が生じます。
VariantInitは空の状態を作り、VariantClearは保持している資源を解放して空の状態へ戻します。この二つを処理の始点と終点に対応させ、VARTYPE、BSTR、SAFEARRAY、COM参照の所有権まで追跡することが、VARIANTを安全に扱うために覚えておくべき要点です。