ホーム < ゲームつくろー! < IKD備忘録

C++
VARIANT型を知ってみる

(2009. 9. 9)


 VARIANT型は何でも格納できる便利な変数で、VBA(Visual Basic for Application)などではお馴染みの型です。と言っても私はVBAをちょこっとしか触った事が無いのですが(^-^;。COMプログラムでサーバ側を作っていると、これをC++で使う必要が出てきます。でも良くわからない・・・ので、調べてみました。



@ VARIANT型とは?

 まずC++側で定義されているVARIANT型の宣言を見てみます:

typedef struct tagVARIANT VARIANT;

oaidl.hというヘッダー内でtagVARIANTのtypedefとして定義されていました。tagVARIANTはこうなっています:

struct tagVARIANT
{
    union
    {
        struct __tagVARIANT
        {
            VARTYPE vt;
            WORD wReserved1;
            WORD wReserved2;
            WORD wReserved3;
            union
            {
                LONGLONG llVal;
                LONG lVal;
                BYTE bVal;
                SHORT iVal;
                FLOAT fltVal;
                DOUBLE dblVal;
                VARIANT_BOOL boolVal;
                _VARIANT_BOOL bool;
                SCODE scode;
                CY cyVal;
                DATE date;
                BSTR bstrVal;
                IUnknown *punkVal;
                IDispatch *pdispVal;
                SAFEARRAY *parray;
                BYTE *pbVal;
                SHORT *piVal;
                LONG *plVal;
                LONGLONG *pllVal;
                FLOAT *pfltVal;
                DOUBLE *pdblVal;
                VARIANT_BOOL *pboolVal;
                _VARIANT_BOOL *pbool;
                SCODE *pscode;
                CY *pcyVal;
                DATE *pdate;
                BSTR *pbstrVal;
                IUnknown **ppunkVal;
                IDispatch **ppdispVal;
                SAFEARRAY **pparray;
                VARIANT *pvarVal;
                PVOID byref;
                CHAR cVal;
                USHORT uiVal;
                ULONG ulVal;
                ULONGLONG ullVal;
                INT intVal;
                UINT uintVal;
                DECIMAL *pdecVal;
                CHAR *pcVal;
                USHORT *puiVal;
                ULONG *pulVal;
                ULONGLONG *pullVal;
                INT *pintVal;
                UINT *puintVal;
                struct __tagBRECORD
                {
                    PVOID pvRecord;
                    IRecordInfo *pRecInfo;
                } __VARIANT_NAME_4;
            } __VARIANT_NAME_3;
        } __VARIANT_NAME_2;
        DECIMAL decVal;
    } __VARIANT_NAME_1;
};

ぎゃー!!!!

 ちょ、ちょっと落ち着いて構造を整理します(^-^;;。大外はこうなっています:

struct tagVARIANT
{
    union
    {
        struct __tagVARIANT
        {
          ...
        } __VARIANT_NAME_2;
        DECIMAL decVal;
    } __VARIANT_NAME_1;
};

unionの下には__tagVARIANT構造体とDECIMAL型の変数の2つです。これらはunionですから相入れない関係で、双方は同じメモリブロックを共有します。__tagVARIANTの中は、

struct __tagVARIANT
{
    VARTYPE vt;
    WORD wReserved1;
    WORD wReserved2;
    WORD wReserved3;
    union
    {
       LONGLONG llVal;
        ...
        UINT *puintVal;
        struct __tagBRECORD
        {
            PVOID pvRecord;
            IRecordInfo *pRecInfo;
        } __VARIANT_NAME_4;
    } __VARIANT_NAME_3;
} __VARIANT_NAME_2;

VARTYPEは確かunsigned short型のtypedefでした(SAFEARRAYの章を参照)。3つのWORD型の予約領域(wReserved1〜3)があって、その下に恐ろしく長いunionがあります。これらのunion内の変数はみな同じメモリブロックを共有します!結局の所、VARIANT型は型情報vtとある1種類の変数を格納する構造体である事がわかります。ちなみに、VARIANT型のサイズは16byteです。予約領域までが8byteで、なが〜い変数群で一番サイズが大きいのがLONGLONG型やDOUBLE型の8byte。なるほど16byteです。見た目によらず小さいですね。


A VARIANT型を使ってみる

 VARIANT型を実際に使ってみます・・・って、どうやるのかわかりません。Webを調べてみるとMSDNに関連情報がありました。VARIANT型に対する数学的な操作はVariant Arithmetic Functionsにあります。また型変換などの操作はVariant Manipulation Functionsにありました。ここにはさらにVARIANTの使い方が書いてありました(^-^)。

 まずVARIANT型は初期化をする必要があります。初期化はVariantInit関数で行います:

VOID VariantInit(
    VARIANTARG *pvarg
);

VARIANTARGって何だ?と思ったら単にVARIANT型のtypedefでした。ここにVARIANT変数を渡すと初期化完了のようです。内部でメモリが動的に確保されているというわけでもないようで、本当に単に構造体を初期化する関数みたいです(間違っているかも)。

 続いて格納したい変数のタイプを指定します。これはVARIANT::vtに渡します。大きく次のパターンに分かれるようです:

フラグ 説明
VT_UI1, VT_I2, VT_I4, VT_R4, VT_R8, VT_BOOL, VT_ERROR, VT_CY, VT_DECIMAL, VT_DATE 実体をそのまま格納。型変換するとポインタ情報が失われます。
VT_BYREF | any type ポインタとして型を格納。実体は呼び出し元にあるとみなされます。
VT_BSTR 文字列を格納。SysAllocString関数でBSTR型の文字列を生成し、SysFreeString関数で解放しましょう。
VT_ARRAY | any type SAFEARRAY型として格納。SafeArrayCreate関数で配列を生成し、SafeArrayDestroy関数で解放しましょう。
VT_DISPATCH, VT_UNKNOWN インターフェイスポインタとして格納。いらなくなったらReleaseメソッドを呼びましょう。

なるほど、vtへのフラグ指定がとても大切のようですね。

 後は実際に値を入れてみます。例えばunsigned int型の値をVARIANT型に入れてみます:

VARIANT vr;
VariantInit( &vr );
vr.vt = VT_UI4;
vr.uintVal = 1000;

構造体には=演算子などは用意されていないので、自分で正しい変数を指定してあげる必要があります。unsigned int型の場合はuintValです。

 使い終わったらVariantClear関数でお掃除するのがマナーのようです:

HRESULT VariantClear(
    VARIANTARG *pvarg
);

引数に有効なVARIANT型を渡すと変数タイプvtをVT_WMPTY(空っぽ)にし変数を全部初期化してしまいます。



B 型変換してみる

 VARIANT型の良さは「型変換」につきます。C++でVARIANT型に格納された変数を型変換するにはVariantChangeType関数を使います:

HRESULT VariantChangeType(
    VARIANTARG *pvargDest,
    VARIANTARG *pvarSrc,
    unsigned short wFlags,
    VARTYPE vt
);

pvargDestには変換後の値を受けるVARIANT型を指定します。一方pvarSrcには変換前のVARIANT型を渡します。両者が同じVARIANT型を指している場合、ちゃんと変換がかかって置き換えられるようです。便利です。
wFlagsは変換に関するオプションフラグです。BOOL型を文字列"TRUE"に変換するなどちょっとマニアックな事をしたい場合にのみ使用するようで、特に指定無しなら0で構いません。
vtには変換したい型タイプをVT_***で与えます。

 これを用いて例えばdouble型の浮動小数点をBSTR型のCOM用文字列に変換する関数を作るとこんな感じになります:

BSTR DoubleToBSTR( double val ) {
    VARIANT vr;
    VariantInit( &vr );
    vr.vt = VT_R8;
    vr.dblVal = val;
    VariantChangeType( &vr, &vr, 0, VT_BSTR );
    return vr.bstrVal;
}

引数に浮動小数点を与えると、文字列として返してくれます。こりゃとても便利。パフォーマンスや細かな仕様を考えない部分でこういう変換作業をしちゃってもいいかもです。


C SAFEARRAYを格納してみる

 最後に関数の中でSAFEARRAYを作ってVARIANT型として出力してみます。この作業はCOMなどにおいて配列を引き取ったり受け渡したりする時に必要となります。例として引数に渡した整数値から素数を抽出して配列で返す関数を作ってみましょう。素数は「エラトステネスの篩」で見つけていきます:

void getPrimeNumber( unsigned val, VARIANT* out ) {
    if ( out == 0 )
        return;

    VariantInit( out );
    out->vt = VT_ARRAY | VT_UI4; // 配列格納宣言

    if ( val <= 1 ) {
        // ※注意:1は素数ではないですよ
        SAFEARRAYBOUND sab = {0, 0};
        SAFEARRAY *ary = SafeArrayCreate( VT_UI4, 1, &sab );
        out->parray = ary; // parrayがSAFEARRAY(へのポインタ)
        return;
    }

    // エラトステネスの篩で素数抽出
    std::vector<unsigned> primeAry; // 素数を格納
    std::vector<bool> isPrime( val + 1, true );
    isPrime[0] = isPrime[1] = false;
    unsigned curTop = 1;
    while ( curTop < val ) {
        if ( isPrime[++curTop] == false )
            continue;
        primeAry.push_back( curTop );
        for ( unsigned int i = curTop * 2; i <= val; i += curTop )
            isPrime[i] = false; // 篩から漏れました
    }

    // SAFEARRAYに素数を書き込み
    SAFEARRAYBOUND sab = {(LONG)primeAry.size(), 0};
    SAFEARRAY *ary = SafeArrayCreate( VT_UI4, 1, &sab );
    out->parray = ary;
    unsigned *p = 0;
    if ( SUCCEEDED( SafeArrayAccessData( ary, (void**)&p ) ) ) {
        memcpy( p, &primeAry[0], sizeof( unsigned ) * primeAry.size() );
        SafeArrayUnaccessData( ary );
    }
}

ポイントがいくつかあります。まずVARIANT型を初期化して「UINT型のSAFEARRAYを作るぜ!」と宣言します(VT_ARRAY | VT_UI4)。素数が出来た後、素数の数分だけの要素を持ったSAFEARRAYを作成し(SafeArrayCreate関数)、結果を書き込みます。作ったSAFEARRAYをVARIANT::parrayに代入して終了です。

 C++で使うにはちょっと色々する必要があるVARIANT型ですが、COMで動的な配列のやり取りをする時に必要になるかと思います。知っていて損は無しです。