■ 仮想関数でフレームワークをつくる
SAMPLE
シンプルウィンドウ サンプル
フレームワークをつくる場合は先に制御フローをつくってしまい
アプリケーション固有の処理を仮想関数をつかってハンドラとして用意してしまう。
OpenGL のフレームワークをつくる。
不変な部分をシステム( フレームワーク )側で用意する。
そして可変な部分を 仮想関数 にしてアプリケーションごとにカスタマイズさせてあげる。
class GLFramework {
public:
// アプリケーション固有の処理の仮想関数として用意しておく。
virtual void onRender();
// 固定の内容は仮想関数にしない
void run();
};
アプリケーションは Framework の流れに沿って必要な部分( カスタマイズ )したい部分を
上書き( オーバーライド )する。
class Sample : public GLFramework {
public:
// 赤くかく
void onRender() {
glClearColor( 1, 0, 0, 1 );
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
}
};
class Sample2 : public GLFramework {
public:
// 青くかく
void onRender() {
glClearColor( 0, 0, 1, 1 );
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
}
};
後は実行するだけ。全体の流れはフレームワークにまかせる。
#include "glframework.h"
int main() {
{
Sample1 app;
app.run();
}
{
Sample2 app;
app.run();
}
return 0;
}
フレームワーク側は共通に必要となる処理を用意しておく。
ウィンドウをつくったり, メッセージループを回す部分は共通の処理なのでしておく。
不変な部分にあわせてシステムをつくる。
GLFramework::GLFramework {
// ウィンドウをつくる
m_hWnd = CreateWindow();
}
メッセージループを回す
void GLFramework::run() {
while ( PeekMessage() ) {
// 仮想関数をよんであげる。これでアプリケーション固有の処理がよばれる。
onRender();
}
}
■ virtual 関数の使いどころ
具体的な処理を派生クラスにまかせるときに使用する。
基本クラスで宣言され、派生クラスで再定義されるメンバ関数
仮想関数の再定義において、prototype が同一のものをoverride : 異なるものを overload
POINT
共通部分を探ることが重要
その部分を virtual 関数にする。
拡張が想定される位置を仮想関数にする。
仮想関数にすることで、関数テーブル経由でメソッドを動的にコールすることができるため
可変になるということ。
カスタマイズ可能という意味。
基底クラス側でハンドラとして用意しておき、実際に利用する側( 派生クラス側 )が具体的な処理をかく。
class Converter {
void output( const string &path );
}
POINT
関数ポインタでも同様のことができる
Application::display(){
virtual void Task::main(); // 具体的な処理は派生先にまかせる。
}
広範囲の集合に対して処理をする際に有効
DESC
virtual は呼ばないと カスタマイズされているかわからない
virtual とは [ 同様にみなせる ] オブジェクトであることを保障する方法
型の継承の意味
POINT
virtual void save();
-> カスタマイズ可能 save() メソッド
// virtual は呼び出す側の再利用を意味する
for( i=0; i< 10; i++ ){
item->save(); // 固有のサービスがコールされる
}
■ InterfaceClass
DESC
interface class を使用することの目的は
共通の規格( ふるまい )を強制すること。
フレームワーク( 呼ぶ )側の再利用が促進される。
■ 抽象クラス
DESC
純粋仮想関数 をもつクラス
POINT
[ 純粋仮想関数だけ ]が定義されているクラスはインターフェースという
純粋仮想関数 を含む class を抽象クラス ( abstract class )と呼ぶ
abstract class は生成できない。しかし pointer は OK
POINT
継承されることを前提としたクラス( デザインパターン的視点 )
純粋仮想関数を含むクラス( 文法的視点 )
POINT
クラスが派生されてからしか決定できないようなデータを使うときに抽象クラスを使用
POINT
抽象基底クラスの意味( 設計的 )
DESC
純粋仮想関数により、フレームワークの提供側としては
default の振る舞い
仮想関数の[ 強制的な使用 ]を示すことになる
Task->Run();
Window->Display();
■ vtable( 仮想関数 table )
DESC
cpp コンパイラが自動で method 第一引数に this ptr をいれていくれる
POINT
vtable とは FunctionPointer Table のこと
Class ごとの情報のひとつ
Debuger でみると __classType という名前がみれる
オブジェクトのディスパッチテーブルはオブジェクトの
動的にバインドされるメソッドのアドレスを保持する。
コンパイラが virtual 宣言を見つけると、テーブルを作成する。
Method の Call は
アドレスをオブジェクトのディスパッチテーブルからとって利用する
ディスパッチテーブルは同じクラスに属するオブジェクトでは全て同じ
通常オブジェクトから共有される。
互換性のある型のオブジェクトは同じレイアウトのディスパッチテーブルを持ち
メソッドのアドレスは
全ての型互換のクラスの中で常に同じオフセットに現れる。
メソッドのアドレスをディスパッチテーブルから取り出すことで
オブジェクトの実際のクラスに対応したメソッドが得られる。
各クラスのインスタンスは関数テーブルへのポインタをもつことになる。
そのため仮想関数をもたないクラスよりポインタのサイズ分が増える。
class Job () {
int i;
public:
virtual void run(){}
};
// 8
Job j;
printf( "size %d" sizeof( j ) );
1. static const char *item[] = {
"run",
"title",
"menu",
"reset",
"exit",
};
for ( int i=0 ; i < sizeof(item)/sizeof(item[0]) ; i++ ) {
2. // 次の2項目を相手に渡す
// 渡し方としては
// 1. ReqAdd の ptr を渡す。
// 2. メンバ関数で id, str を渡す. こちらの方がスマート
//
// スマートでない方法は data[] = { data0, data1, data2 ... } サイズの計算がメンドイ
// tgt->sendSignal( sig, data );
//
// 送るべき内容を message Class としてまとめてしまう。
//
// 各 message はそのサービスをするクラスが内包すれば OK
//
1. code での マクロの反映方法( rebuild は必要 )
#if defined(_WIN32)
vsprintf(d.top, form, arg);
#else
vsnprintf(d.top, LINE_SIZE, form, arg);
#endif
■ 関数のオーバーライド
DESC
sub cls で base cls の関数を overwrite しないならば、base cls のものが使用される
sub cls で宣言した 仮想関数は, 実装しないとコンパイラに怒られる。
POINT
ABC の実装はすべての派生先で必要か ?
一度でも 実装すれば、後の派生先は実装しなくても OK
virtual static を併用可能か ?
DESC
できない( ERR: virtual メンバ関数は静的メンバ関数にはなれません )
■ オーバーロード
関数のオーバーロードとは同一スコープで同じ名前をもつが signature が異なる関数こと。
class Test {
void func( int );
void func( float );
};
POINT
signature とは次の構成のこと
関数名 ( パラメータ型と順序 ) const, volatile の有無
以下は異なる signature
void func( int ) const ;
void func( int );
オーバーライドは異なるスコープで同一の signature をもつこと。
派生クラスで仮想関数テーブルを上書きしてしまうこと。
class Base {
virtual void func();
};
class Derive : public Base {
void func();
};
■ クラス継承間での同名の関数
派生クラスと基底クラスで同名の関数があると、内部スコープの名前が外部スコープの名前をかくす。
そのため、このような状態にならないような名前をつけることが大切。
class Base {
void func( int );
};
class Derive : public Base {
// 同じ名前
void func( float );
};
Derive d;
// Derive::func( float )が呼ばれる
// 派生先の名前 ( func )が基底クラスの名前を隠してしまうということ
d.func( 10 )
POINT
1個のクラスはひとつのスコープになる。
■ 仮想関数のコンストラクタの呼び出し
コンストラクタ内で仮想関数を呼ぶとそのクラスのメソッドが呼ばれる。
もし派生先のメソッドが呼ばれると 派生のメンバを操作することになるためこの動作は正しい。
class Derive : public Base {
void func();
};
Base::Base {
// これは Base::func() が呼ばれる。
func();
}
■ 仮想コンストラクタ
WARNING
コンストラクタは仮想関数にできない。
理由は 基底クラスのコンストラクタがコールされている時点では
派生クラスのメンバが初期化されていないから。
■ スコープ演算子をつかう
仮想関数内で、スコープ演算子をつかうことでクラスのメソッドを明示できる。
仮想関数テーブルを経由せずに( パイパスして )明示的に呼ぶときに使う。
class Gui {
virtual void move() {}
};
void Button::move() {
// 基底クラスの move() を再利用する
Gui::move();
// Button 固有の move() 処理をかく
}
クラスの利用者はこの機能を通常はつかわない。
クラスの開発者が、派生クラスのメソッド、コンストラクタ、デストラクタ内で便宜上つかう。
■ 純粋仮想関数(PureVirtualFunction)
WARNING
抽象基底クラスは デストラクタの "定義" をもつ必要がある。
リンカーが派生クラスのデストラクタから呼ばれる Base::~Base() を探すため。
class Base {
public:
virtual ~Base() = 0;
};
class Derive : public Base {
};
int main() {
Derive d;
return 0;
}
定義すればリンクエラーにならない。
派生クラスをインスタンス化するには、純粋仮想関数のすべてを実装する必要がある。
このときに、継承元をたどって実装されていればいい。
要は仮想関数テーブルが埋まっていればいいということ。
class Base {
public:
virtual void f() = 0;
virtual void g() = 0;
};
class Derive1 : public Base {
void f();
};
class Derive2 : public Derive1 {
void g();
};
■ 非インライン関数
仮想関数をもつクラスはひとつの 非インラインメソッドをもつべき。
class Base {
public:
virtual ~Base();
};
class Derive : public Base {
};
int main() {
return 0;
}