Entry

プログラミングメモ - Static Reflector パターン

2010年08月25日

C++ のデザインパターンで,そういえば,邦書ではまり触れられていないな,と思ったパターンを紹介します。つか,日本でもパターンとか言わずにやってる人はやってるんですが(ま,そゆのをデザパタというのか)。

ここで取り上げる Static Reflector パターンは,C の関数と C++ のクラスを橋渡しするのに使われるパターンです。C++ のプログラムを書く場合,自分では全て C++ のプログラムを書いていても,C で書かれた(定義された)関数を使わなくちゃいけないときがあるんですね。例えば,ライブラリや OS の API を使うような場合です。Windows の API を C++ のクラスでラップしようと目論んだ方は結構いるはず。

で,C++ のクラスでラップするとき,ほとんどの関数は単純にメンバ関数で目的の関数を呼べばいいんですけれど,一部の API では一筋縄ではいかない場合があります。それが,関数ポインタを渡す形式の関数です。Windows API の CopyFileEx や __beginthread のような関数(__beginthread はマクロだが),POSIX Pthread API の関数では,ユーザが処理の中身を定義して,その処理(コールバック関数)へのポインタを渡すことになっています。ここで困ることになります。

例えば、Windows API の CopyFileEx 関数の場合,コピーの進捗を管理するためにコールバック関数を定義することができます。CopyFileEx のプロトタイプは次の通り。

BOOL CopyFileEx(
  LPCWSTR lpExistingFileName,           // 既存ファイルの名前
  LPCWSTR lpNewFileName,                // 新規ファイルの名前
  LPPROGRESS_ROUTINE lpProgressRoutine, // コールバック関数
  LPVOID lpData,                        // コールバック関数に渡すデータ
  LPBOOL pbCancel,                      // 操作の取り消しに使う
  DWORD dwCopyFlags                     // ファイルのコピー方法を指定する
);

第3引数でコールバック関数を渡せるんですね。渡せる関数のプロトタイプは次の通りです。

DWORD CALLBACK CopyProgressRoutine(
  LARGE_INTEGER TotalFileSize,          // バイト単位の総ファイルサイズ
  LARGE_INTEGER TotalBytesTransferred,  // 転送された総バイト数
  LARGE_INTEGER StreamSize,             // ストリームの総バイト数
  LARGE_INTEGER StreamBytesTransferred, // ストリームに転送された総バイト数
  DWORD dwStreamNumber,                 // 現在のストリーム
  DWORD dwCallbackReason,               // この関数が呼び出された理由
  HANDLE hSourceFile,                   // コピー元ファイルのハンドル
  HANDLE hDestinationFile,              // コピー先ファイルのハンドル
  LPVOID lpData                         // CopyFileEx 関数から渡されるデータ
);

これを C++ のクラスでラップする場合,次のようなクラス設計が考えられます。

class CopyFile {
public:
  CopyFile() {}
  virtual ~CopyFile() {}
public:
  BOOL execute(LPCTSTR src, LPCTSTR dst) {
    CopyFileEx(src, dst, progress, NULL, FALSE, 0);
  }
protected:
  // ファイルコピーの進捗管理用コールバック関数(引数は省略)
  // 継承して自分用に定義したい。
  virtual DWORD CALLBACK progress(...) {
    // 基底クラスでは何もしない
  }
};

しかし,これではうまくいきません。なぜか。

それは,コールバック関数として渡すことができる関数ポインタが静的(static)である必要があるからです。関数 progress をオブジェクトのメンバ関数として宣言してしまうと,コンパイルすることができません。

それじゃ,関数 progress を静的にすればいいじゃないか,とも考えられます。これならコンパイルは通ります。しかし,これもうまくいかない。どうしてかというと,クラス関数(クラス内の静的関数)は継承できないからです。また,静的関数は,オブジェクトのメンバにアクセスできないので,この点でもオブジェクトごとに異なる処理を作ることができません。

で,こゆときに役立つのが,Static Reflector パターンです。このパターンを使うと,静的なコールバック関数をオブジェクトのメンバ関数にディスパッチすることができます。オブジェクトのメンバ関数にできるということは,継承可能でメンバ参照可能にもなるということです。

では,中身をば。前置きが長かったけれども,やってることは簡単です。先ほどの CopyFileEx 関数をクラスでラップしてみます。まず,宣言は以下の通り。

#ifndef COPY_FILE_H
#define COPY_FILE_H

#include <windows.h>
#include <tchar.h>

namespace sys {
class CopyFile {
public:
  CopyFile() throw ();
  virtual ~CopyFile() throw ();
private:  // no implement
  CopyFile(const CopyFile& other) throw ();
  CopyFile& operator=(const CopyFile& rhs) throw ();
public:
  BOOL execute(LPCTSTR existingFileName, LPCTSTR newFileName);
protected:
  virtual DWORD progress(
    LARGE_INTEGER totalFileSize, LARGE_INTEGER totalBytesTransferred,
    LARGE_INTEGER streamSize, LARGE_INTEGER streamBytesTransferred,
    DWORD streamNumber, DWORD callbackReason,
    HANDLE hSourceFile, HANDLE hDestinationFile);
private:
  // (1)
  static DWORD CALLBACK reflect(
    LARGE_INTEGER totalFileSize, LARGE_INTEGER totalBytesTransferred,
    LARGE_INTEGER streamSize, LARGE_INTEGER streamBytesTransferred,
    DWORD streamNumber, DWORD callbackReason,
    HANDLE hSourceFile, HANDLE hDestinationFile, LPVOID data);
};
}

#endif  // WIN__COPY_FILE_H

実装はこちら。

#include "CopyFile.h"

namespace sys {

CopyFile::CopyFile() throw () {
}

CopyFile::~CopyFile() throw () {
}

BOOL
CopyFile::execute(LPCTSTR existingFileName, LPCTSTR newFileName) {
  // (2)
  return CopyFileEx(existingFileName, newFileName, reflect, this, FALSE, 0);
}

DWORD
CopyFile::progress(
  LARGE_INTEGER totalFileSize, LARGE_INTEGER totalBytesTransferred,
  LARGE_INTEGER streamSize, LARGE_INTEGER streamBytesTransferred,
  DWORD streamNumber, DWORD callbackReason,
  HANDLE hSourceFile, HANDLE hDestinationFile) {
  // do nothing
  return 0;
}

DWORD CALLBACK
CopyFile::reflect(
  LARGE_INTEGER totalFileSize, LARGE_INTEGER totalBytesTransferred,
  LARGE_INTEGER streamSize, LARGE_INTEGER streamBytesTransferred,
  DWORD streamNumber, DWORD callbackReason,
  HANDLE hSourceFile, HANDLE hDestinationFile, LPVOID data) {
  // (3)
  CopyFile* cp = static_cast<CopyFile*>(data);
  DWORD ret = 0;
  if (cp != 0) {
    ret = cp->progress(
      totalFileSize, totalBytesTransferred, streamSize, streamBytesTransferred,
      streamNumber, callbackReason, hSourceFile, hDestinationFile);
  }
  return ret;
}

}

テストドライバは次のようなものです。foo.txt を bar.txt にコピーします。

#include <iostream>
#include "CopyFile.h"

int
main(int argc, char* argv[]) {
  sys::CopyFile cp;
  if (cp.execute(_T("foo.txt"), _T("bar.txt"))) {
    std::cout << "success." << std::endl;
  } else {
    std::cout << "failed." << std::endl;
  }
  return 0;
}

順番に見ていきましょう。

まずポイントとなるのが,クラス宣言部(1)で static なクラス関数 reflect を宣言しているところ。CopyFileEx に渡すコールバック関数には,こいつを指定します。で,指定するのはいいんですけれど,それと一緒に,CopyFileEx 関数にはオブジェクトのアドレス自身も渡します(実装部(2))。引数にコールバックを指定する API 関数は,ほとんどの場合,ユーザが定義したデータを渡すために void* 型のアドレスを取ります。こいつに,オブジェクト自身のアドレス(this)を渡すわけです。これで CopyFileEx は,何かのイベントが起こるとそれを呼んだオブジェクトのアドレス付きで,コールバック関数 reflect を呼ぶことになります。

ここまでできれば,あとは簡単。関数 reflect は this ポインタを受け取るので,こいつを元の型(CopyFile* 型)にキャストして,本命のオブジェクト関数 progress を呼ぶことになります。関数 progress は,仮想関数として宣言されているので,ユーザは CopyFile クラスを継承して自分で定義した進捗管理を行うことができます。もちろん,進捗管理にあたって,何らかのデータが必要な場合は(進捗ダイアログウィンドウのハンドラとか),メンバ変数として定義しておくこともできます。

継承した例として,クラス CopyFile を継承した MyCopyFile クラスを示します。まず宣言部。

#ifndef MY_COPY_FILE_H
#define MY_COPY_FILE_H

#include "CopyFile.h"

class MyCopyFile : public sys::CopyFile {
public:
  MyCopyFile() throw ();
  virtual ~MyCopyFile() throw ();
private:  // no implement
  MyCopyFile(const MyCopyFile& other) throw ();
  MyCopyFile& operator=(const MyCopyFile& rhs) throw ();
protected:
  virtual DWORD progress(
    LARGE_INTEGER totalFileSize, LARGE_INTEGER totalBytesTransferred,
    LARGE_INTEGER streamSize, LARGE_INTEGER streamBytesTransferred,
    DWORD streamNumber, DWORD callbackReason,
    HANDLE hSourceFile, HANDLE hDestinationFile);
};

#endif  // MY_COPY_FILE_H

続いて定義部。

#include <iostream>
#include "MyCopyFile.h"

MyCopyFile::MyCopyFile() throw () {
}

MyCopyFile::~MyCopyFile() throw () {
}

DWORD
MyCopyFile::progress(
  LARGE_INTEGER totalFileSize, LARGE_INTEGER totalBytesTransferred,
  LARGE_INTEGER streamSize, LARGE_INTEGER streamBytesTransferred,
  DWORD streamNumber, DWORD callbackReason,
  HANDLE hSourceFile, HANDLE hDestinationFile) {
  double total = static_cast<double>(totalFileSize.QuadPart);
  double trans = static_cast<double>(totalBytesTransferred.QuadPart);
  double rate = ((total > 0) ? trans / total : 0.0) * 100.0;
  std::cout << rate << " % transferred." << std::endl;
  return 0;
}

関数 progress を上のようにオーバーライドすれば,何パーセントコピーが終了したか画面に表示されます。この要領で,GUI ウィジェットのプログレスバーと連携させることもできます。

実のところ,冒頭で書いたとおり,この方法は知ってる人は普通にやってるものだったりします。一番有名なところでは,Windows API を使ったプログラミングで中核のコールバック関数となるウィンドウプロシージャを,クラスの中に入れ込む場合です。ウィンドウ全般を取り扱うクラスとして,Window クラスを作る場合,ウィンドウプロシージャは static にしなくちゃいけないわけですけれど,Static Reflector パターンを使えば,オブジェクトごとに継承可能なウィンドウプロシージャを持つことができます。ウィンドウプロシージャは,void* 型の引数を持っていないんですけれど,SetWindowLongPtr/GetWindowLongPtr 関数を使って,this ポインタをやり取りすることができます。

一方,この方法が適用できないのは,見てお分かりの通り,void* 型などのポインタでユーザデータを受け渡しできない API をラップする場合です。例えば,signal(2) なんかには適用できません。this ポインタをやり取りできないので,仕方ありません。

このエントリの元になったリソースはたくさんあるんですけれど,例えば「The Static Reflection Pattern」[PDF]が挙げられます(公開されているリソースでは Reflection となっているが,Reflector という方が多い気がする)。有名なのに日本語のリソースでは紹介されていないパターンって,意外とあるんですよね。他にもあるので,折を見てまた紹介ます。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN