C++ ⇒ VBA 書いて覚えるための初心者自己中記事

C++ ⇒ VBA 勉強の履歴を付けるというかノート代わりに使ってます

C++ Windows プログラミング⑤ 書いて覚えるための初心者自己中記事

COM  オブジェクトの有効期間を管理する。

 

全てのCOMインターフェイスは、インターフェイスIUnknownを直接・間接的問わず継承する必要がある。

なぜならこのインターフェイスIUnknownはCOMオブジェクトがサポートしなければならない基本機能を提供しているから。

その基本機能が3つあって、

QueryInterface

AddRef

Release

だ。

QueryInterfaceメソッドがあると、実行時にプログラムがオブジェクトの機能をクエリ出来るようになる。

クエリって検索的な意味でいいのかな。

AddRefとReleaseはこれから勉強するオブジェクトの有効期間の管理に使用する。

 

 

オブジェクトの有効期間ていうのは

C++でいうところのデストラクタ的な話か。いつどうやって開放するか。

COMでは参照カウントというアプローチをとる。

COMでは参照カウントと言われる内部カウントを保持している。

あるオブジェクトを参照している数をカウント。

現在有効なオブジェクトへの参照数。

参照が0になったオブジェクトは自身を削除する。

寂しいと死んじゃうのね。

 

オブジェクト作成時点でカウントは1になり、プログラム上ではオブジェクトのポインターが1つ存在する。

このポインターは複製できる。AddRefメソッドを利用することでポインターを複製するとオブジェクトのカウントは1増える。

オブジェクトを参照しているポインターを破棄するときはReleaseメソッドを呼び出す。ポインターが無効になり、オブジェクトの参照カウントが1減る。

そのオブジェクトを参照しているポインターがすべてReleaseされるとオブジェクトは自身を削除する。

 

ポインターのスコープが消える前にReleaseしないとオブジェクトはカウントを0に出来ないので削除されない。

 

よし、これは分かりやすかった。

つぎ。

 

オブジェクトにインターフェイスを要求する。

あ、さっき疑問にしてた、(前の記事か・・)オブジェクトとインターフェイスで名称が全然違うのは何?の答えがある。

オブジェクトは複数のインターフェイスを実装できる。

CmmonItemDialogオブジェクトは

IFileOpenDialogインターフェイス(一般的な使用方法の大部分をサポート)

IFileDialogCustmizeインターフェイス(高度な使用方法)

などほかにもあるみたい。図があった。

Common Item Dialog オブジェクトによって公開されるインターフェイスの図

 

さっき言ってたIUnknownインターフェイスあった。

クラスの継承みたいなものかな。

CoCreateInterface関数でだとGUIDの指定で好きなインターフェイスポインタ貰えるのかな?

説明読むと、すでにIFileOpenDialogインターフェイスのポインタを持ってて、もちろんCmmon Item Dialogのオブジェクトが実体化されている状態でIFileDialogCustomizeインターフェイスのポインタを取得する方法が書いてある。

Cmmon Item Dialogオブジェクトが実体化されているからCoCreateInterface関数は使えないよね。

COMではこの場合オブジェクトに要求するらいし。

COMならみんな持ってるIUnknownインターフェイスのメソッドQueryInterfaceをつかう。

HRESULT QueryInterface(REFIID riid, void **ppvObject);

こうなってる。

第一引数は要求するインターフェイスのGUID

第二引数はおなじみのポインタ

hr = pFileOpen->QueryInterface(IID_IFileDialogCustomize, 
    reinterpret_cast<void**>(&pCustom));
if (SUCCEEDED(hr))
{
    // インターフェイスを使用する  (省略)
    // ...

    pCustom->Release();
}
else
{
    // エラーを処理する
}

こうなる。

 

よし、大丈夫。

つぎ、

COMのメモリ割り当て

 メソッドによって、メモリバッファーをヒープに割り当て、バッファーのアドレスを返す場合がある。

COMではメモリの割り当てと解放を行う関数がある。

 

というかメソッドと関数って違うの?

~~~~~~~~~~~~~

オブジェクトの動きを操作するものがメソッドなのか。

あ、それで、メモリの割り当てと解放を行う関数。

 

CoTaskMemAlloc関数 メモリブロックを割り当てる

CoTaskMemFree関数 上記で割り当てられたメモリブロックを解放

 

 2つ前くらいの記事でやったファイルを開くダイアログボックスでやったプログラムに入ってた

PWSTR pszFilePath;//PWSTR(wchar_t*)を用意
hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);//ShellItemオブジェクトのメソッド・GetDisplayNameがファイルパスを文字列で取得
 
// ユーザーにファイル名を表示する
if (SUCCEEDED(hr))
{
	MessageBox(NULL, pszFilePath, L"File Path", MB_OK);//ファイルパスを文字列で取得したpszFilePathをMessageBoxで表示
	CoTaskMemFree(pszFilePath);//CoTskMemFree関数でメモリ解放

 GetDisplayNameメソッドで取得したファイルパス文字列

このメソッドが文字列のメモリをCoTaskMemAlloc関数で割り当てているらしい。

CoTskMemFree関数で開放するのはプログラムに記述しないといけない。

 

よし、つぎ。

COMのコーディングプラクティス

 ~堅牢で効率的なコードの作り方~

だそうです。

 

 

 えー、なになに?

プログラムをビルドするときに

unresolved external symbol "struct _GUID const IID_IDrawable"

このようなエラーが出ることがある。

GUIDが外部リンケージ(extern)で宣言されており、定数の定義をリンカーが特定できなかったというエラー。

細かい説明がよくわからないな。

__uuidof演算子を使うと回避できる。

CoCreateInstance関数を使う場合に

IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL, 
    __uuidof(pFileOpen), reinterpret_cast<void**>(&pFileOpen)); 

 GUIDにつける。

でもこれだけやっても全然ダメだ。肝心のGUID部分が認識されない。

 

: GUID 値と型名を関連付けるには、ヘッダーで __declspec(uuid( ... )) を宣言します。詳細については、Visual C++ ドキュメントの __declspec に関する説明を参照してください。

 

 何だろうね。検索しても要領を得ない。

とりあえずこんなのあるんだ~、と思っておこう。

あ、動いた。GUIDのプレフィックスを消すのか。

じゃあ注意書きはますますなぞだね。

 

 

えー、用意したインターフェイス型ポインタと関数内で指定したGUIDが違っていたということがあり得る。

// 誤った記述

IFileOpenDialog *pFileOpen;

hr = CoCreateInstance(
    __uuidof(FileOpenDialog), 
    NULL, 
    CLSCTX_ALL, 
    __uuidof(IFileDialogCustomize),       // この IID はポインター型と一致しない
    reinterpret_cast<void**>(&pFileOpen)  // void** に強制型変換する
    );

 

これを回避するには IID_PPV_ARGSマクロを使う。

__uuidof(IFileDialogCustomize), reinterpret_cast<void**>(&pFileOpen)

これを

 
IID_PPV_ARGS(&pFileOpen)

こうする。

第四・第五引数がまとまった。

これはCoCreateInstance関数でも

QueryInterfaceメソッドメソッドでも使える。

これはあれか?引数にしたポインタの型を確認してくれるのか?

 

つぎ。参照カウントのはなしだ。

使用後のインターフェイスポインターをReleseしなかった。

無効なポインターでReleseを呼んだ。

Relese呼び出し後にインターフェイスポインターの逆参照が行われた。

 

これらを回避する方法。

適切に処理してくれる関数があって、

template <class T> void SafeRelease(T **ppT)
{
    if (*ppT)
    {
        (*ppT)->Release();
        *ppT = NULL;
    }
}

これはなるほど。

 

あ、これは自分で書いて使うんだね。はい。

 

SafeRelease(&pFileOpen);//オブジェクトの参照カウントを減らす。

 2回かけてもいいくらい、らしい。

 

つぎ、

COMスマートポインター

インターフェイスポインターのReleaseとかをC++のコンストラクタ・デストラクタのように自動で行ってくれると嬉しい。

コピーコンストラクターや代入演算子オーバーロード、COMポインターへのアクセス。これらもちゃんと用意したクラスがVisual studioのアクティブテンプレートライブラリ(ATL)にある。

ATLスマートポインタークラスはCComPtrという名前。

 このCComPtrクラスを使うと

こうだったのが↓

IFileOpenDialog *pFileOpen;

 こうなる↓ 

CComPtr<IFileOpenDialog> pFileOpen;

 テンプレートクラス的な。

 

で、こうだったのが↓

hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL, __uuidof(IFileOpenDialog), reinterpret_cast<void**>(&pFileOpen));

 こうなる↓

hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));//CoCreateInstance関数がメンバ関数に。引数もクラス識別子だけ。

 インターフェイスポインタ取得が簡単。

 

GetResuleメソッドでもらっていたIShellItemインターフェイスポインタも

IShellItem *pItem;

 ↓

CComPtr<IShellItem> pItem;

 これで作る。

hr = pFileOpen->GetResult(&pItem);

 これは同じ↑

 

Release関数使ってた所はというと

						//SafeRelease(&pItem);//オブジェクトの参照カウントを減らす。
		}//pItemがスコープから外れる
	}
	//SafeRelease(&pFileOpen);//オブジェクトの参照カウントを減らす。
}//pFileOpenがスコープから外れる

スコープから外れると、Releaseが呼ばれるらしい。

-> と

& は演算子オーバーロードしているそうで、ポインタのように使える。

素晴らしい。楽。

 

よしつぎ、COM最後のページ。

COMのエラー処理。

 HRESULT型に格納されるエラーコードは様々なSDKヘッダーが様々な定数を定義している。

システム全体の共通コードはWinError.hに入っている。

 

定数 数値 説明
E_ACCESSDENIED 0x80070005 アクセスが拒否されました。
E_FAIL 0x80004005 予測できないエラーです。
E_INVALIDARG 0x80070057 パラメーターの値が無効です。
E_OUTOFMEMORY 0x8007000E メモリが不足しています。
E_POINTER 0x80004003 ポインター値に誤って NULL が渡されました。
E_UNEXPECTED 0x8000FFFF 予期しない状態です。
S_OK 0x0 成功しました。
S_FALSE 0x1 成功しました。

 一部らしい。

HRESULT型の戻り値がある関数・メソッドを使ったら常にSUCCEEDEDマクロかFAILEDマクロを使用してチェックする。

 

S_OKはいい。

S_FALSEはなにか?

これはマイナスの状態であるが失敗ではない。という意味で使われることがある。

メソッドが成功したが影響がない場合。

CoInitializeEX関数が同じスレッドから2回目に呼び出された場合にS_FALSEを返す。

 

エラー処理のパターン

大事なこと

  • HRESULT を返すすべてのメソッドまたは関数が、次の処理に進む前に戻り値をチェックしている。
  • リソースの使用後、これを解放する。
  • NULL ポインターなど、無効または初期化されていないリソースにアクセスしない。
  • 解放した後のリソースを使用しない。

 

パターン1

if文をネストする。

この場合は最小限のスコープでよくなる。

それぞれのifブロックが終わるタイミングでRelease出来る。

エラーが発生してもその部分以降は変に処理されないで終われる。

 

デメリットは見難い(個人的に)

 

パターン2

if文を重ねる。 

    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->Show(NULL);
    }
    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->GetResult(&pItem);
    }

ブロック内で次のif文の条件になる処理を行う。

見やすい。

初期化・解放がまとめて最初と最後に行われる。

エラー後もやる必要のないifが続けられる。

 

パターン3

goto文を使う。

if文の条件でFAILEDを使って、エラー時の処理はgotoでまとめて行う。

分かりやすいし完結で見やすい。

ただし、初期化はgotoより前に全て持って行かないといけない。

 

 

パターン4

try throw catch を使う。

#include <comdef.h>  // _com_error を宣言する

inline void throw_if_fail(HRESULT hr)
{
    if (FAILED(hr))
    {
        throw _com_error(hr);
    }
}

void ShowDialog()
{
    try
    {
        CComPtr<IFileOpenDialog> pFileOpen;
        throw_if_fail(CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
            CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen)));

        throw_if_fail(pFileOpen->Show(NULL));

        CComPtr<IShellItem> pItem;
        throw_if_fail(pFileOpen->GetResult(&pItem));

        // pItem を使用する (省略)
    }
    catch (_com_error err)
    {
        // エラーを処理する
    }
}

 

このthrow_if_fail関数すごい。

 

おぉ、COMの初心者用説明文終わった。

難しいな。

Windowsデベロッパーセンターの初心者用ページ

まだまだ、つづくし。

ここまで。