1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

100万記事ありがとう!2025年もたくさんアウトプットしよう

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

Posted at

はじめに

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

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

image.png


1. VARIANTとは

image.png

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

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

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

image.png

(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 の呼び忘れ

  • VARIANTBSTRSAFEARRAY などが格納されている場合、使い終わったら 必ず 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 の解放ミス

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

image.png

  1. 不正な vt に対する VariantClear

    • vt がスタックのゴミ値や、実際のメンバと矛盾しているときに起こる。
    • 例:vt=VT_BSTR だが bstrVal が不正ポインタを指している…など。
  2. SysAllocStringLenSysReAllocStringLen の引数ミス

    • 長さ指定を誤って実際の領域より少ないサイズを確保してしまい、アクセス違反を起こす。
  3. 多次元 SAFEARRAY のサイズ・次元数を間違えた解放

    • VariantClear 時に配列が正しくない構造だと、内部処理でエラーやクラッシュが発生する。

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

image.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 を使う

    • 手動で bstrValparray をコピーするより安全。
    • コピー先の VARIANT がすでに使用されている場合は先にクリア(VariantClear(&dstVar))しておく。
  5. RAII(Resource Acquisition Is Initialization)で自動解放

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

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

image.png

#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

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?