はじめに
VC++(Microsoft Visual C++)でCOMを扱う際に利用頻度の高い構造体の一つに VARIANT
があります。
VARIANT
は多種多様な型(文字列、配列、オブジェクトなど)を保持できる便利な仕組みですが、初期化や解放の手順を誤ると簡単にメモリリークや例外(アクセス違反)を引き起こしてしまうため、注意が必要です。
本記事は自分用の備忘録として、VARIANT
まわりでありがちな失敗パターンと対策をまとめています。
1. VARIANTとは
VARIANT
は COM で使われる汎用的なデータ型です。具体的には以下のような特徴があります。
- 中身の型を表す
vt
(VARIANT.vt
)と、実際の値を格納するユニオンがセットになっている。 -
VARIANT
はBSTR
(COMで使われる文字列型)やIDispatch*
、IUnknown*
、SAFEARRAY*
などを格納できる。 - メモリ管理(文字列や配列の領域確保・解放)を自力で行わなければならないケースがあるため、使い方を誤るとメモリリークやクラッシュに直結する。
2. よくある失敗パターン
(1) VARIANT
の初期化漏れ
-
VARIANT
を宣言した後、必ずVariantInit(&var)
を呼ぶ必要がある。 -
VariantInit
を呼ばずにいきなりメンバ変数に値を代入したり、VariantClear
で解放しようとすると、スタック上のゴミ値が原因でメモリ破壊やクラッシュを招く。
// NG: 初期化漏れ
VARIANT var;
var.vt = VT_BSTR; // var.vtの初期値が不定のまま…
// OK: きちんとVariantInitで初期化する
VARIANT var2;
VariantInit(&var2); // var2.vt = VT_EMPTYなどに初期化
(2) VariantClear
の呼び忘れ
-
VARIANT
にBSTR
やSAFEARRAY
などが格納されている場合、使い終わったら 必ずVariantClear(&var)
で解放しなければならない。 - COM関数から返された
VARIANT
も、呼び出し側が後処理をしないとリークする可能性大。
// BSTRが格納されたVARIANTの例
VARIANT varStr;
VariantInit(&varStr);
varStr.vt = VT_BSTR;
varStr.bstrVal = SysAllocString(L"Hello World");
// 処理が終わったら解放
VariantClear(&varStr);
(3) COM関数呼び出し後の解放漏れ
- 例:
IDispatch::Invoke
の戻り値やOutパラメータで受け取るVARIANT*
。
受け取ったVARIANT
に文字列や配列などが割り当てられているなら、呼び出し側が 責任を持ってVariantClear
する必要がある。
VARIANT varResult;
VariantInit(&varResult);
hr = pDisp->Invoke(
dispID,
IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_METHOD,
&dispParams,
&varResult,
nullptr,
nullptr
);
if (SUCCEEDED(hr)) {
// varResultの内容を使用
}
// 使用後は必ず解放
VariantClear(&varResult);
(4) SAFEARRAY
の解放ミス
-
VARIANT
がVT_ARRAY
(VT_ARRAY | VT_I4
やVT_ARRAY | VT_VARIANT
など)を保持している場合は、基本的にはVariantClear
を呼べば自動でSafeArrayDestroy
が走る。 - ただし、マルチディメンション配列や要素が複雑な場合(
VT_RECORD
など)で特別な解放が必要なときは、手動でSafeArrayDestroy
などを呼び出す場合がある。 - 何らかの独自管理をしているなら、必ず解放方法を確認してから使う。
VARIANT varArr;
VariantInit(&varArr);
// SafeArrayCreateVectorなどで配列を作成 (詳細は省略)
varArr.vt = VT_ARRAY | VT_I4;
varArr.parray = pSafeArray;
// 通常ならVariantClearでOK
VariantClear(&varArr);
(5) VariantCopy
のmisuse
-
VariantCopy
は SRC(コピー元)の内容を DST(コピー先)にまるごと複製する。 - DST に既に有効な
VARIANT
が入っている場合、先にVariantClear(&dst)
しておく か、まっさらなVariantInit(&dst)
状態にしないとリークやクラッシュを起こす。
VARIANT var1, var2;
VariantInit(&var1);
VariantInit(&var2);
var1.vt = VT_BSTR;
var1.bstrVal = SysAllocString(L"test");
// var2がなんらかの有効な値を保持している場合は事前にVariantClear
VariantCopy(&var2, &var1);
// var2にも"test"がコピーされる
VariantClear(&var2);
VariantClear(&var1);
3. 例外やクラッシュにつながる典型例
-
不正な
vt
に対するVariantClear
-
vt
がスタックのゴミ値や、実際のメンバと矛盾しているときに起こる。 - 例:
vt=VT_BSTR
だがbstrVal
が不正ポインタを指している…など。
-
-
SysAllocStringLen
やSysReAllocStringLen
の引数ミス- 長さ指定を誤って実際の領域より少ないサイズを確保してしまい、アクセス違反を起こす。
-
多次元
SAFEARRAY
のサイズ・次元数を間違えた解放-
VariantClear
時に配列が正しくない構造だと、内部処理でエラーやクラッシュが発生する。
-
4. 対策・ベストプラクティス
-
必ず
VariantInit
で初期化する- 宣言直後に
VariantInit(&var)
を呼んで、vt=VT_EMPTY
にしておく。 - あるいは
VARIANT var = { 0 };
と書いてもよいが、明示的にVariantInit
の方が意識しやすい。
- 宣言直後に
-
使い終わったら
VariantClear
する-
VARIANT
にリソースが割り当てられているなら、スコープを抜ける前にVariantClear(&var)
を忘れない。 -
エラー処理 の分岐や
return
が複数ある関数では、とくに注意。
-
-
受け取った
VARIANT
の解放責任を明確にする- COM メソッド(例:
IDispatch::Invoke
)などで戻ってきたVARIANT
は、呼び出し側がVariantClear
する前提が多い。 - 社内独自APIなどを作る場合も、呼び出し側が後始末すべきかどうかをドキュメント化しておく。
- COM メソッド(例:
-
複製は
VariantCopy
を使う- 手動で
bstrVal
やparray
をコピーするより安全。 - コピー先の
VARIANT
がすでに使用されている場合は先にクリア(VariantClear(&dstVar)
)しておく。
- 手動で
-
RAII(Resource Acquisition Is Initialization)で自動解放
- C++ ならデストラクタで
VariantClear
を呼ぶラッパークラスを作成し、スコープを抜けたら自動で解放する仕組みにする。 - RAIIを使えば、例外が発生しても確実に解放が走るため、ヒューマンエラーを大きく減らせる。
- C++ ならデストラクタで
5. RAIIラッパークラスのサンプル
#include <windows.h>
#include <oleauto.h>
#include <stdexcept>
#include <iostream>
class VariantGuard {
public:
VariantGuard() {
VariantInit(&m_var);
}
~VariantGuard() {
VariantClear(&m_var);
}
// コピーコンストラクタ・代入演算子は禁止(必要なら実装)
VariantGuard(const VariantGuard&) = delete;
VariantGuard& operator=(const VariantGuard&) = delete;
VARIANT* get() { return &m_var; }
const VARIANT* get() const { return &m_var; }
private:
VARIANT m_var;
};
int main() {
try {
VariantGuard varGuard;
VARIANT* pVar = varGuard.get();
// BSTRを割り当ててみる
pVar->vt = VT_BSTR;
pVar->bstrVal = SysAllocString(L"Hello RAII Variant");
// ここで例外が起きてもデストラクタが自動的に呼ばれて解放される
// throw std::runtime_error("error!");
std::wcout << L"VARIANT text: " << pVar->bstrVal << std::endl;
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
// ここでは何もしなくてもOK。VariantGuardのデストラクタで解放される
}
return 0;
}
- このように、初期化漏れや解放忘れを防ぐ定番テクニックです。
6. まとめ
-
VARIANT
は多種多様な型を扱える反面、メモリ管理を慎重に行わないと簡単にリークやクラッシュに繋がる要注意な型。 - 主な注意点:
- 宣言直後に
VariantInit
- 使用後に
VariantClear
- 複数箇所からの return やエラー処理でもクリア漏れしない
- COM関数のOutパラメータは呼び出し側が責任をもって解放
-
VariantCopy
は使い方に注意(コピー先を空にしてからコピー)
- 宣言直後に
- RAII を使ってラッパークラスを作れば、例外や複雑な制御フローでも後始末を自動化でき、事故を大幅に減らせる。
この記事が、VC++で VARIANT
を扱うときに困った際の一助になれば幸いです。