【備忘録】VC++のVARIANTでハマりがちなメモリリーク・例外対策

## はじめに
VC++（Microsoft Visual C++）でCOMを扱う際に利用頻度の高い構造体の一つに `VARIANT` があります。  
`VARIANT` は多種多様な型（文字列、配列、オブジェクトなど）を保持できる便利な仕組みですが、**初期化や解放の手順を誤ると簡単にメモリリークや例外（アクセス違反）を引き起こしてしまう**ため、注意が必要です。

本記事は自分用の備忘録として、`VARIANT` まわりでありがちな失敗パターンと対策をまとめています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/12735c9f-161a-96f1-f1a5-c7fb85b3b82d.png)

---

## 1. VARIANTとは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/22f395f2-e687-c493-5b80-7d025ea8ec57.png)

`VARIANT` は COM で使われる汎用的なデータ型です。具体的には以下のような特徴があります。

- 中身の型を表す `vt`（`VARIANT.vt`）と、実際の値を格納するユニオンがセットになっている。
- `VARIANT` は `BSTR`（COMで使われる文字列型）や `IDispatch*`、`IUnknown*`、`SAFEARRAY*` などを格納できる。
- メモリ管理（文字列や配列の領域確保・解放）を**自力で行わなければならない**ケースがあるため、使い方を誤るとメモリリークやクラッシュに直結する。

---

## 2. よくある失敗パターン

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/800b1dd5-1dad-2244-0d50-77e5c6140c8b.png)

### (1) `VARIANT` の初期化漏れ

- `VARIANT` を宣言した後、**必ず** `VariantInit(&var)` を呼ぶ必要がある。
- `VariantInit` を呼ばずにいきなりメンバ変数に値を代入したり、`VariantClear` で解放しようとすると、スタック上のゴミ値が原因でメモリ破壊やクラッシュを招く。

```cpp
// 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` も、呼び出し側が後処理をしないとリークする可能性大。

```cpp
// 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` する必要がある。

```cpp
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` などを呼び出す場合がある。
- 何らかの独自管理をしているなら、必ず解放方法を確認してから使う。

```cpp
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)` 状態にしないとリークやクラッシュを起こす。

```cpp
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. 例外やクラッシュにつながる典型例

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/425e32bb-9aff-bf6b-4575-0507c2b97c0b.png)

1. **不正な `vt` に対する `VariantClear`**  
   - `vt` がスタックのゴミ値や、実際のメンバと矛盾しているときに起こる。
   - 例：`vt=VT_BSTR` だが `bstrVal` が不正ポインタを指している…など。

2. **`SysAllocStringLen` や `SysReAllocStringLen` の引数ミス**  
   - 長さ指定を誤って実際の領域より少ないサイズを確保してしまい、アクセス違反を起こす。

3. **多次元 `SAFEARRAY` のサイズ・次元数を間違えた解放**  
   - `VariantClear` 時に配列が正しくない構造だと、内部処理でエラーやクラッシュが発生する。

---

## 4. 対策・ベストプラクティス

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/2de1b088-774c-28fe-31a2-9ac5b79d1dda.png)

1. **必ず `VariantInit` で初期化する**  
   - 宣言直後に `VariantInit(&var)` を呼んで、`vt=VT_EMPTY` にしておく。
   - あるいは `VARIANT var = { 0 };` と書いてもよいが、明示的に `VariantInit` の方が意識しやすい。

2. **使い終わったら `VariantClear` する**  
   - `VARIANT` にリソースが割り当てられているなら、スコープを抜ける前に `VariantClear(&var)` を忘れない。
   - **エラー処理** の分岐や `return` が複数ある関数では、とくに注意。

3. **受け取った `VARIANT` の解放責任を明確にする**  
   - COM メソッド（例：`IDispatch::Invoke`）などで戻ってきた `VARIANT` は、呼び出し側が `VariantClear` する前提が多い。
   - 社内独自APIなどを作る場合も、呼び出し側が後始末すべきかどうかをドキュメント化しておく。

4. **複製は `VariantCopy` を使う**  
   - 手動で `bstrVal` や `parray` をコピーするより安全。
   - コピー先の `VARIANT` がすでに使用されている場合は先にクリア（`VariantClear(&dstVar)`）しておく。

5. **RAII（Resource Acquisition Is Initialization）で自動解放**  
   - C++ ならデストラクタで `VariantClear` を呼ぶラッパークラスを作成し、スコープを抜けたら自動で解放する仕組みにする。
   - RAIIを使えば、例外が発生しても確実に解放が走るため、ヒューマンエラーを大きく減らせる。

---

## 5. RAIIラッパークラスのサンプル

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/94f83893-928c-52ac-9bbf-c93993844aa9.png)

```cpp
#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. まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/0c992b26-8fe1-cfa7-0908-dcef2ddd28e0.png)

- `VARIANT` は**多種多様な型を扱える反面、メモリ管理を慎重に行わないと簡単にリークやクラッシュに繋がる**要注意な型。
- 主な注意点：
  1. **宣言直後に `VariantInit`**  
  2. **使用後に `VariantClear`**  
  3. **複数箇所からの return やエラー処理でもクリア漏れしない**  
  4. **COM関数のOutパラメータは呼び出し側が責任をもって解放**  
  5. **`VariantCopy` は使い方に注意**（コピー先を空にしてからコピー）  
- **RAII** を使ってラッパークラスを作れば、例外や複雑な制御フローでも後始末を自動化でき、事故を大幅に減らせる。

---

この記事が、VC++で `VARIANT` を扱うときに困った際の一助になれば幸いです。