ATLAS日本基礎ネットワーク C++トレーニングコース
ここでの目標
目次
クラスというものが、プログラムの問題領域の中で振る舞うオブジェクト〜物体を規定する型であると言うことを説明しました。振る舞いは主にメソッドすなわちメンバー関数で規定されており、利用者はそのクラスのオブジェクトを生成し、そのメソッドを起動する(呼び出す)ことで仕事をするというのが作業パターンとなっています。
例として3次元の図形処理プログラムを考えてみましょう。まず、直方体を実装してみましょう。
class Vector3D; //前方宣言(forward declaration) 3次元ベクトルを想定 class GC; //同じく前方宣言。グラフィックスコンテキストを想定 class Box { public : Box( double w, double h, double d ); ~Box( ); void move( Vector3D & pos ); void rotate( Vector3D & axis, double angle ); void draw( GC & gc ); private: double m_width; double m_height; double m_depth; Vector3D m_position; Vector3D m_axis; double m_rotation_angle; };
コンストラクタBox()
は引数を持っていて、箱の寸法を初期化するようです。メソッドとしては移動(move
)や回転(rotate
)が出来、さらに描画(draw
)が可能なことがわかります。
同様に円柱Column
を作りましょう。
class Vector3D; class GC; class Column { public: Column( double length, double radius ); ~Column( ); void move( Vector3D & pos ); void rotate( Vector3D & axis, double angle ); void draw( GC & gc ); private: double m_length; double m_radius; Vector3D m_position; Vector3D m_axis; double m_rotation_angle; };
Box
とColumn
は非常に良く似たクラスになっています。メンバー関数にもメンバーデータにも共通なものがあります。実装についても考えてみましょう。move
メソッドの可能な実装としては
void Box::move( Vector3D & pos ) { m_position = pos; //Vector3Dクラスでは代入演算子が定義されているとします。 }
たぶん、Column::move
も全く同じ定義になるのではないでしょうか。さらにrotate
も
void Box::rotate( Vector3D & axis, double angle ) { m_axis = axis; m_rotation_angle = angle; }
これもColumn::rotate
と同じ定義が使えます。まず経済的コーディングの観点から、同じことを二度実装するのはもったいない。共通部分move
やrotate
、m_position
、m_axis
やm_rotation_angle
を集めて一度だけ記述すればよいと言うようにできないかと考えるのは自然です。
一方プログラミングモデルとしてこれらの共通部分が何を表しているかを考えてみましょう。Box
もColumn
も図形であるのだから、移動したり回転したり出来る。move
やrotate
はBox
やColumn
固有の性質ではなく、「図形」一般が持つ性質ではないか。逆に「図形」とは「移動できるもの」「回転できるもの」ということで定義できるのではないか。「箱」や「円柱」より「図形」はより抽象的な「もの」として扱えるのではないか。
クラスの継承はこのような考えから生まれてきました。
まず、より抽象的なクラスを定義します。それを基底クラス(親クラス)として、そのクラスを継承し、派生クラス(子クラス)を作ります。今の例ではShape
クラスを基底クラスとすることにしましょう。
class Shape { public: void move( Vector3D & pos ); void rotate( Vector3D & axis, double angle ); protected: Vector3D m_position; Vector3D m_axis; double m_rotation_angle; };
アクセス制御のラベルにprotected:
というものが出てきました。private
ですと、このクラスを継承した子クラスはそのメンバーにアクセスできません。protected
とすることで、公開メンバーではないメンバーを継承クラス内部で参照可能になります。もちろんprotected
メンバーは外部からは直接アクセスすることは出来ません。
このShape
クラスを元にしてBox
クラスを作りましょう。
class Box : public Shape { public: Box( double width, double height, double depth ); ~Box( ); void draw( GC & gc ); private: double m_width; double m_heidht; double m_depth; };
クラス定義の最初のところで、このBox
クラスがShape
クラスをパブリックに継承していると宣言します。
パブリックの継承とは、親クラスのアクセス制御をそのまま引き継ぐ継承です。
public
メンバーはpublic
に、protected
メンバーはそのままprotected
になります。protected
に継承すると、public
なメンバーがprotected
として扱われ、protected
メンバーはprivate
として扱われるようになります。通常はpublic
な継承を使います。
クラスメンバーは、親クラスShape
に含まれないものを記述します。コンストラクタは箱の寸法を決めます。draw
メンバー関数は現在のパラメータに従って箱を描こうとするでしょう。
同様にColumn
クラスも
class Column : public Shape { public: Column( double length, double radius ); ~Column( ); void draw( GC & gc ); private: double m_length; double m_radius; };
これも同様です。draw
というBox
と同じメソッドを持っていますが、こちらのdraw
は円柱を書こうとするもので、実装は明らかにBox
とは異なります。
作業場所を用意しておきましょう。
cd ~/tutorial/cplusplus
mkdir l5
cd l5
図形の例を実習しても良いのですが描画をしたりする実装が簡単ではないので別の例を考えてみます。端末に文字を表示するアプリケーションを考えてみます。例えば端末に-
を並べて横線を引きましょう。そのためのクラスを考えます。Line
クラスとしましょう。Line.h
を書きます。
#ifndef __LINE_H #define __LINE_H #include <iostream> class Line { public: Line( int plength ); void draw( std::ostream & os ); private: int m_length; }; #endif
実装はLine.cxx
です。
#include "Line.h" Line::Line( int plength ) : m_length( plength ) { } void Line::draw( std::ostream & os ) { for( int i = 0; i < m_length; i ++ ) os << '-'; }
テストのためにmain
関数を書きます。main.cxx
としましょう。
#include "Line.h" main( ) { using namespace std; Line l(40); l.draw( cout ); }
走らせます。分割コンパイルです。
g++ -c Line.cxx
g++ -c main.cxx
g++ main.o Line.o
./a.out
------------------------------
改行されないので少しかっこうわるいです。LineNew
クラスを加えます。LineNew.h
は
#ifndef __LINENEW_H #define __LINENEW_H #include <iostream> class LineNew { public: LineNew( int plength ); void draw( std::ostream & os ); private: int m_length; }; #endif
実装はお任せします。LineNew.cxx
を作ってください。Line.cxxとほぼ同じで、単に最後に改行するだけです。main.cxx
を
#include "Line.h" #include "LineNew.h" main( ) { using namespace std; Line l(40); LineNew ln(20); l.draw( cout ); ln.draw( cout ); }
実行します。
g++ -c LineNew.cxx
g++ -c main.cxx
g++ main.o Line.o LineNew.o
./a.out
------------------------------
今度はちゃんと改行されたでしょうか。
非常にシンプルな例ですが、std::ostream
に出力できるものという共通性が見て取れます。これら二つのクラスLine
とLineNew
の基底クラスとしてDrawer
クラスを考えます。Drawer.h
は
#ifndef __DRAWER_H #define __DRAWER_H #include <iostream> class Drawer { public: void draw( std::ostream & os ); }; #endif
実装を一応しておきましょう。
#include "Drawer.h" void Drawer::draw( std::ostream & os ) { }
書くべきものは持たないので何もしません。Line
とLineNew
を継承させましょう。Line.h
は
#ifndef __LINE_H #define __LINE_H #include "Drawer.h" class Line : public Drawer { public: Line( int plength ); void draw( std::ostream & os ); private: int m_length; }; #endif
Line.cxx
の方は特に変更は必要ありません。同様にLineNew.h
を書き換えてください。main.cxx
もそのままにしておきましょう。でもヘッダーファイルの内容が書き換わっているので全部コンパイルし直します。
g++ -c Drawer.cxx
g++ -c Line.cxx
g++ -c LineNew.cxx
g++ -c main.cxx
g++ main.o Drawer.o Line.o LineNew.o
./a.out
------------------------------
継承のありがたみは今ひとつわかりませんが、問題なく動くことは確認できました。
継承クラスは、自分が継承した親クラスのメンバーと自分自身のメンバーを持つことになります。
既に見てきたように、継承を考慮して、アクセス制御はpublic
、protected
、private
という3種のカテゴリーに分類されることになります。
public
は公開メンバーを表します。ATLASではメンバーデータの公開を禁止していますので、基本的にメソッドのみになります。
protected
は継承クラスにアクセスを許す非公開メンバーです。
private
は継承クラスにアクセスを許さない非公開メンバーです。例えば基底クラスの中で厳重に管理されており、子クラスのメソッドから不注意に操作されたくないメンバーデータはprivate
に設定します。
ATLASでは、クラス定義の中ではまずpublic
、続いてprotected
、最後にprivate
メンバーを宣言するように規則を決めています。
継承クラスのコンストラクタは親クラスのコンストラクタを明示的に呼び出すことが出来ます。コンストラクタの関数定義において
Box::Box( double width, double height, double depth ) : Shape( ), m_width( width ), m_height( height ), m_depth( depth ) { ... }
というようにコンストラクタShape()
を呼び出しています。今の場合、明示しなくてもデフォルトコンストラクタであるShape()
は自動的に呼び出されますが、例えば引数を持ったコンストラクタを呼び出すときには明示が必要になります。
実行順序にも注意しましょう。まず最初に親クラスのコンストラクタShape()
が呼び出され、続いてメンバーデータの初期化子が実行されます。最後に{}
が実行されることになります。
子クラスで、親クラスと同じ名前のメンバーを定義することが出来ます。この場合、親クラスのメンバーは遮蔽され、見えなくなります。
先ほどの実習のmain
関数を見てみましょう。
main( ) { using namespace std; Line l(40); LineNew ln(20); l.draw( cout ); ln.draw( cout ); }
これを実行した結果からわかるようにl.draw
やln.draw
は継承クラスLine
やLineNew
のクラスのdraw
メソッドが呼ばれています。基底クラスDrawer
のdraw
メンバー関数は遮蔽されていて使われません。
Drawer d; d.draw( cout );
をmain
関数の適当な場所に加えて実行しましょう。これは実際にはなにもしない(正しく)基底クラスDrawer
のdraw
メソッドが呼ばれます。
Box
もColumn
もdraw
という名前のメソッドを持っています。これはmove
やrotate
メソッドと違って、実装は異なります。ですが、図形が描画できるものという概念から言えばShape
のメソッドと考えたくなります。そのために仮想継承という仕組みを導入します。
仮想継承とは、メソッドを仮想的virtual
と宣言することで、基底クラスで仕様を決めるが、その実装は派生クラスごとに行えるようにします。
class Shape { ... public: virtual void draw( GC & gc ); ... };
これを継承する側は
class Box : public Shape { ... public: virtual void draw( GC & gc ); ... };
Shape::draw
メソッドはたぶん何もしないまま制御が帰ってくるでしょう。一方、Box::draw
の方は箱の絵を描くことになるでしょう。
この例では派生クラスの定義でも
virtual
宣言をしています。親クラスで一度virtual
と宣言されたメソッドはその子クラスでも一貫してvirtual
として扱われるため、子クラス側ではvirtual
宣言はしなくても良いのですが、ATLASの規則としては明示することを要求しています。
では、このメソッドをどのように使えば良いかというと
Shape * drawables[ 10 ]; ... drawables[ 0 ] = new Box( 1.0, 2.0, 3.0 ); drawables[ 1 ] = new Column( 2.0, 3.0 ); drawables[ 2 ] = new Sphire( 1.5 ); ... for( int i = 0; i < 10; i ++ ) drawables[ i ] -> draw( gc ); ...
Shape
型へのポインター配列drawables
をまず定義しました。そしてその要素にShape
型を継承したいろいろなクラスのオブジェクトを生成して加えます。for
ループの中ではdrawables
のおのおのの要素についてdraw
メソッドが呼ばれています。Shape
クラスはdraw
メソッドを持っています。それ故、構文上正しい表現になっています。しかし、実際に呼び出されるのはShape
のdraw
メソッドではなく、個別の子クラスのオブジェクトのdraw
メソッドです。
ここで非常に重要な概念が導入されたことがわかります。例えば次のような関数を考えましょう。
void drawAll( Shape ** shapelist, int entries ) { GC gc; Shape ** sp = shapelist; for( int i = 0; i < entries; i ++ ) * sp ++ -> draw( gc ); }
この関数はShape
型が定義されさえすれば実装することが可能です。この関数内に限って言えば誰もShape
がどのように継承されたかを意識していません。Shape
型という抽象的なクラスを用いてプログラムが書けてしまいます。もちろんこの関数が意味を持つのは、別のところでShape
を継承した様々なクラスが実装され、それらを詰め込んだshapelist
が渡された時なのですが。
実習サンプルDrawer
に戻りましょう。Drawer
のdraw
メソッドは上述の議論から当然仮想メンバーであるべきでしょう。Drawer.h
を次のように修正します。
#ifndef __DRAWER_H #define __DRAWER_H #include <iostream> class Drawer { public: virtual void draw( std::ostream & os ); }; #endif
他は特に変更ありません。全体のコンパイルをやり直して実行してみましょう。
g++ -c Drawer.cxx
g++ -c Line.cxx
g++ -c LineNew.cxx
g++ -c main.cxx
g++ main.o Drawer.o Line.o LineNew.o
./a.out
------------------------------
そろそろこれを繰り返して書くのが面倒になってきました。コンパイル・リンクの手順を書いておいてそれを呼び出す方が楽です。Makefile
というファイルを用意します。内容は次のようになります。
.SUFFIXES: .cxx .cxx.o: g++ -c $< OBJECTS = Drawer.o Line.o LineNew.o l5.exe: main.o libl5.a g++ -o l5.exe main.o -L. -ll5 libl5.a: $(OBJECTS) ar -r libl5.a $(OBJECTS) main.o: Drawer.h Line.h LineNew.h Drawer.o: Drawer.h Line.o: Drawer.h Line.h LineNew.o: Drawer.h LineNew.h
ここで段下げ(インデント)は必ずタブキーを使って下げてください。空白だと正しく動きません。HTML表示の関係で上の記述は段下げが空白で書かれていますが、これをそのままコピペしたのではだめですのでご注意ください。
先にオブジェクトファイルを消しておきます。
rm *.o
では今用意したMakefile
を使ってコンパイル・リンクをやりましょう。
make
g++ -c main.cxx
g++ -c Drawer.cxx
g++ -c Line.cxx
g++ -c LineNew.cxx
ar -r libl5.a Drawer.o Line.o LineNew.o
g++ -o l5.exe main.o -L. -ll5
make
もしくはgmake
と呼ばれるコマンドを使ってみました。make
では
目的ファイル
:
依存ファイル
という形でファイルの関係を書き、目的ファイルより依存ファイルの方が新しいとその次に書かれたコマンドを実行します。コマンドが書かれていない場合は目的ファイルのサフィックスから処理手順を決めます。目的ファイルが.o
ファイルで、サフィックスルールが決まっているものはその元になるファイルを探し、それがあればルールに従って処理をします。今の場合サフィックスルールは.cxx.o:
という形で書かれておりその実体はg++ -c $<
です。$<
は入力ファイル(目的ファイルのサフィックス.o
を取り、代わりにサフィックスルールに書かれたソースのサフィックス.cxx
をつけたもの)を表します。以後、いずれかのファイルを書き直すとそれによって依存するファイルがすべて作り直されます。便利ですね。
さて、仮想メソッドを導入したメリットを確認しておきましょう。main.cxx
を書き直します。
#include "Line.h" #include "LineNew.h" main( ) { using namespace std; Drawer * d[ 3 ]; d[ 0 ] = new Line( 40 ); d[ 1 ] = new LineNew( 20 ); d[ 2 ] = new Drawer( ); for( int i = 0; i < 3; i ++ ) d[ i ] -> draw( cout ); }
今度はDrawer
クラスへのポインター配列dを定義し、中身をnew
でクラスオブジェクトを生成して加えていきます。実際に描画をさせるところではDrawer
型へのポインターを使ってdraw
メソッドを呼び出していますが、それぞれ正しく継承クラスのdraw
が呼び出されます。確認しましょう。
make
g++ -c main.cxx
g++ -o l5.exe main.o -L. -ll5
上の例の、Shape
クラスのメンバー関数draw
についてもう少し考えてみましょう。皆さんはもしこのメソッドが呼ばれたら何をしますか。「何もしないで返す。」という人もいるかもしれません。
void Shape::draw( GC & gc ) { }
しかし、この関数が呼ばれると言うことは、Shape
型を継承したクラスのオブジェクトでなくShape
型のオブジェクトを作って呼んだ人がいると言うことを表します。これは本来正しい使い方ではないので、
void Shape::draw( GC & gc ) { std::cout << "あなたはShape型のオブジェクトを作っています。良くないよ!" << std::endl; }
このメッセージを見た利用者はコードをチェックして、Shape
のオブジェクトがなぜ出来たか調べるでしょう。
もっと積極的に、Shape
のオブジェクトを文法的に作れなくしてしまう方がよい、必ず派生クラスを使うように利用者に強制する方法がある方が好ましい。そのために純粋仮想関数が導入されました。クラス定義の中で、
class Shape { ... public: virtual void draw( GC & gc ) = 0; ... };
なんと、関数を0
で初期化してしまいます。この意味はShape
クラスの中ではdraw
は実装しません、という宣言です。一つでもこのような純粋仮想関数を持つクラスは抽象クラスと呼ばれ、オブジェクトを作ることが禁止されます。必ず継承したクラスを用意して、そのクラスを使うことになります。
抽象クラスは大規模なフレームワークに利用者が開発した部品を組み込むのに非常に適した仕組みです。自分で開発した部品をフレームワークに組み込みたいプログラマーは、そのフレームワークが提供する抽象クラスをベースにして自分の部品を開発すればよいことになります。少なくとも、すべての純粋仮想関数を実装しさえすれば、そのフレームワークの中で使える部品になります。この意味で、このような抽象クラスをインターフェースと呼びます。
ATLASの解析ソフトウエアAthena
というのはそのようなフレームワークの一つです。Athena
の中で使われるアルゴリズムやサービスはすべてインターフェースクラスを継承して作ることになります。
Drawer
クラスのdraw
メソッドを純粋仮想関数とします。
#ifndef __DRAWER_H #define __DRAWER_H #include <iostream> class Drawer { public: virtual void draw( std::ostream & os ) = 0; }; #endif
これでgmake
をやると
gmake
g++ -c main.cxx
main.cxx: In function `int main()':
main.cxx:19: error: cannot allocate an object of type `Drawer'
main.cxx:19: error: because the following virtual functions are abstract:
Drawer.h:8: error: virtual void Drawer::draw(std::ostream&)
gmake: *** [Main.o] Error 1
Draw
クラスのオブジェクトを生成している部分がエラーになります。必要な機能が実装されていないクラスは使ってはいけないとコンパイラーが警告しています。大規模かつ複雑な継承を行って作られたユーザのクラスが正しく動くことを保証するために重要な機能です。
コンストラクタは派生クラスのオブジェクトとして生成されるときは当然その派生クラスのコンストラクタが呼び出され、その実装の中で基底クラスのコンストラクタが呼び出されるのが普通です。一方、デストラクタには注意が必要です。
仮想的でないデストラクタは、それが宣言されたポインターの形により呼ばれ方が変わってきます。
Box * sp = new Box( 1.0, 2.0, 3.0 ); ... delete sp;
この例ではBox
型のポインターとしてsp
が宣言されているので、delete
文で呼び出されるのはBox
のデストラクタBox::~Box( )
です。正しい処理が期待されます。一方、
Shape * sp = new Box( 1.0, 2.0, 3.0 ); ... delete sp;
このように派生クラスであるBox
のオブジェクトがShape
型へのポインターsp
によって指されています。例の最後でdelete
文により、生成されたオブジェクトが破棄されます。このとき、呼び出されるデストラクタはsp
がShape
型へのポインターであることからShape::~Shape()
になってしまいます。もし、Box
のコンストラクタの中でメモリー割付などがされていると、通常はBox::~Box()
の中でメモリー解放がされているはずなので、このようにデストラクタが正しく呼び出されないとメモリーリークが生じます。
こういった事態を避けるために、デストラクタをvirtual
だと宣言します。
class Shape { ... public: virtual ~Shape( ); ... };
こうすることにより、後の例でsp
がShape
へのポインターであっても、デストラクタが仮想的なために正しくBox::~Box()
が呼び出されます。
仮想メンバーを持ったクラスのデストラクタは必ずvirutal
とすることがATLASでは要求されています。
スコープについては既に説明しましたが、継承はスコープにどのような影響を与えるか見ていきましょう。スコープとは、ものの名前を探してゆく範囲の決め方でした。複文の中という非常に狭いものから大域的なものまでいろいろとあります。クラススコープはその中で比較的広いものです。
派生クラスは派生クラスとしてのスコープを持っています。基底クラスのメンバー関数やメンバーデータはそのまま派生クラスで使われますが、もし、同じ名前のメンバー関数やメンバーデータを派生クラス内で宣言すると、基底クラスのそれは遮蔽されてしまいます。仮想関数などはそれにあたり、派生クラス側のメソッドから仮想関数を呼び出すと、当然派生クラスのメソッドが使われます。
このとき、明示的に基底クラスのスコープを宣言することによって親クラスのメソッドを呼び出すことが出来ます。スコープ演算子と呼び、::
で表します。
class DerivedClass : public BaseClass { ... virtual void vfunc( int iarg ); ... };
例えばDerivedClass
の中から、BaseClass
のvfunc
を使いたいときは
BaseClass::vfunc( 1 );
この意味はBaseClass
というクラススコープを使ってvfunc
という名前を探しなさいと言うことになります。
複数の親クラスを持った派生クラスを定義することが出来ます。これを多重継承と呼びます。クラス継承の例としてインターフェースとしての抽象クラスを見ました。ユーザはフレームワークに自分の部品を繋ぎこみたい。この場合、そのクラスのオブジェクトがいくつかの場面で使用されるとき、その場面ごとにインターフェースが必要になるが、オブジェクトとしては単一であり一貫した情報を保持しているという状況を考えます。
例を挙げてみましょう。Athena
の中でAlgorithm
クラスを継承してユーザのアルゴリズムを書くことが出来ます。このアルゴリズムは開始時に初期化メソッドが呼ばれ、終了時に終了化メソッドが呼ばれ、イベントを処理するたびにユーザ実行メソッドが呼ばれます。一方で、このクラスは属性を設定することが出来、その属性はjobOptions
のスクリプトで与えることが出来ます。このことからAlgorithm
クラスはathena
のステートモデルに従った振る舞いをするものとしてIAlgorithm
クラスを継承するとともに、属性を設定できるものとしてIProperty
クラスを継承します。
class Algorithm : public IAlgorithm, public IProperty {...};
というように親クラスをコンマで区切って並べます。
多重継承をした場合、すべての親クラスのメンバーが引き継がれます。場合によっては同じ名前のメンバー関数やメンバーデータがないとも限りません。その場合、派生クラスの側ではどちらのものが呼ばれているのかを明示する必要があります。
class A { ... public: void print( ); ... }; class B { ... public: void print( ); .... }; class C : public A, public B {...};
A
もB
もprint
と言うメソッドを持っています。
C cobj; //class Cのオブジェクト cobj.print( ); //どちらのprintだかわからない。 cobj.A::print( ); cobj.B::print( );
ここで出てきたA::
というのはA
というクラスのスコープを使うという演算子です。これについては前節で説明しました。
今仮に、A
というクラスを継承してB
を定義し、また別にA
を継承してC
を作ったとします。
class A { protected: int m_identifier; };
A
はメンバーデータとしてm_identifier
という変数を持っています。
class B : public A {...};
さらには
class C : public A {...};
B
もC
もA
を基底クラスとしているのでそれぞれm_identifier
という変数を持っています。さて、このB
とC
の両方を継承したクラスD
を作るとどうなるでしょうか。
class D : public B, public C {...};
元々はA
から始まっているのでこういう関係を菱形継承と呼びます。継承のルールとして、それぞれの親クラスのメンバーを引き継ぐわけですから、B::A::m_identifier
とC::A::m_identifier
の二つのメンバーデータが別々に確保されることになります。これはちょっと都合が悪い。元々m_identifier
というのはオブジェクトの固有番号のつもりでつけており、D
クラスのオブジェクトも一つユニークなm_identifier
を持つべきでしょう。クラスA
は多重継承されても一つだけ引き継いでほしい。そういう場合に仮想継承を使います。
class B : virtual public A {...}; class C : virtual public A {...};
このようにvirtual
に継承することにより、親の親が同一の場合メンバーは一つだけ引き継ぎます。
大規模なソフトウエアシステムでは多重継承を使うことにより合理的に階層を実現することが出来ます。その場合仮想継承は重要な機能です。この節の最初の例、Algorithm
も実は
class Algorithm : virtual public IAlgorithm, virtual public IProperty {...};
として定義されています。
ATLASのルールとしてはメンバーデータを持ったクラスの多重継承を避けるようになっています。上述のような複雑な問題を避けるためです。多重継承して良いのは
抽象インターフェースクラスだけにすべきだと規定しています。
これまで見てきた例では、箱や、円柱などを分析し、そのより抽象的な表現である「型」にたどり着きました。どのように継承を設計すればよいか考えていきましょう。
プログラムの開発をするということは、まず実現したいタスクをどのように表現するかという検討から始まることになります。プログラミングモデルが必要です。問題領域を想定し、その空間の中で振る舞うオブジェクトを発見することが第一歩になります。
次の重要なステップはオブジェクトの関係を検討することです。オブジェクトの関係はおよそ次の三つに分類することが出来ます。
プログラムの中で同じ役回りをするオブジェクトを見つけることは継承関係の発見の手がかりになります。一般的な議論ではなくて、ある特定の問題の解法において、プログラム上の振る舞いとして共通性があるか。いろいろなオブジェクトを同じ椅子に座らせる意味があるか。
クラスの利用者としては、そのフレームワークの開発者がどのようなモデルでそのインターフェースを設計したかを理解することが重要です。AthenaのAlgorithmやServiceがどのように使われようとしているか見てください。
オブジェクト指向解析設計について一度勉強してみることをお薦めします。UML(Unified Modeling Language)と呼ばれる表記法(クラスを箱で表したり、所有関係(has a)、継承関係(is a)、利用関係(using)などを線で表したりするもの)もよく使われます。
クラススコープを越えると次のスコープとしてはこれまでの知識だと大域と言うことになってしまいます。例えばクラス名は大域で探されることになりますが、非常に大規模なソフトウエアのシステムではこれはいささか都合が悪くなります。別々に開発された二つのパッケージを使おうとしたときに、その中に同じ名前のクラスが全く独立に定義されていると、もうコンパイルできません。
こういう事態を避けるために、以前はクラス名にパッケージの略称などを含ませるなどして名前の衝突を避けてきました。しかし、こういうことをするとクラス名が長くなり、プログラムも読みにくくなります。仮にそういう仕組みを置いてもまだ衝突の可能性は残されます。
この問題を解決するために名前空間namespace
が導入されました。クラスや関数などを定義するときに名前空間を宣言します。
namespace MyNamespace { class MyApp { ... //メンバー定義など }; }
この{}に囲まれた範囲で宣言や定義された名前(この例ではMyApp
クラス)はすべてMyNamespace
という名前空間にいることになります。
このように名前空間はクラススコープの外側にスコープの範囲を設定します。一旦名前空間内に定義された名前は、その名前空間に属すため、それを使うためには何らかの方法で名前空間を指定することが必要になります。例えば上の例ではコンストラクタMyApp::MyApp()
を指定するのに
namespace MyNamespace { MyApp::MyApp( ) { ... } }
とするか、
using namespace MyNamespace; MyApp::MyApp( ) { ... }
とするか、
MyNamespace::MyApp::MyApp( ) { ... }
とする必要があります。この例の場合、MyApp::MyApp()
をどこから探すかが確定すれば良いわけです。
一方、main
の中でMyApp
を作りたいときは、
main( ) { MyNamespace::MyApp myapp; }
とか
main( ) { using namespace MyNamespace; MyApp myapp; }
とします。次の場合は都合が悪くなります。
namespace MyNamespace { main( ) { MyApp myapp; }
この場合、main
までがMyNamespace
の中になければならなくなります。通常のmain
関数ではなく、MyNamespace
でユーザが定義された別のmain
関数となり、制御が最初にここに渡されることはありません。
これまでHello! World.
の例でもすでに名前空間は扱ってきました。C++の入出力などはstd
という名前空間内で定義されてきました。そのような新しい名前空間で定義を行っているヘッダーファイルには.h
をつけないということで、以前から使われてきたiostream.h
と区別しています。以前から使われてきたものは名前なし(匿名)名前空間にあると考えます。
匿名名前空間にあるものを参照したいときは
int errorcode = ::errno;
というように::
(スコープ演算子)の前に名前を与えないことで匿名空間にあることを表します。
using namespace
は任意の場所で宣言できます。その有効範囲は通常の変数のスコープと同じです。関数内で宣言されたusing namespace
はその関数の末尾まで有効です。関数外で宣言された場合、例えば#include <iostream>
の直後にusing namespace std;
と書くなどは、ファイルスコープが適用されるので、そのファイルの末尾まで有効になります。ATLASではファイルスコープの使用をしないようにしています。コーディングルールとしてはそれが必要な最小の範囲で宣言します。最初の例、HelloWorld
で、main
関数の中でusing namespace std;
を宣言したのは、それが必要な範囲がmain
関数の中だけだからです。
複数の開発者が同じソースを編集する場合、ファイルスコープのように有効範囲が広いものを導入することは、同じソースの他の部分にまで影響を与えてしまう可能性があります。自分の責任のある部分だけで有効な宣言をすべきだという立場です。例えばファイル前半をAさんが、後半をBさんが書いていて、ある理由で、このファイルを二つに分けたとしましょう。前半にファイルスコープを宣言していてそれが後半に影響していると、分割されたファイルでファイルスコープの見直しをしなくてはいけなくなります。こういうことを避けるためにファイルスコープを使わないようにしています。
名前空間を使ってみましょう。DrawerExample
という名前空間を使うことにします。全部のクラス定義、実装をnamespace
の中に入れます。例えばDrawer.h
は
#ifndef __DRAWER_H #define __DRAWER_H #include <iostream> namespace DrawerExample { class Drawer { public: virtual void draw( std::ostream & os ) = 0; }; } #endif
クラス実装の方も
#include "Drawer.h" namespace DrawerExample { void Drawer::draw( std::ostream & os ) { } }
他のクラスも同様にnamespace DrawerExample { }
で取り囲んでください。また、main.cxx
は
#include "Line.h" #include "LineNew.h" main( ) { using namespace std; using namespace DrawerExample; Drawer * d[ 2 ]; d[ 0 ] = new Line( 40 ); d[ 1 ] = new LineNew( ); for( int i = 0; i < 2; i ++ ) d[ i ] -> draw( os ); }
Line
とLineNew
だけではおもしろくありません。例えばテキストメッセージを文字列で表示するString
クラス、整数の値を適当なフォーマットで表示するIValue
クラス、同じ数値を文字'#'
の数で棒グラフのように表示するIBar
クラスなどを作ってみましょう。それらを組み合わせて、前回の実習で作成したMyHistogram
を棒グラフで表示するサービスクラスHistogramDrawer
クラスを作ってみましょう。継承関係をどうするか、メンバーデータをどうするか、他のオブジェクトの関係をどう実装するか、色々な設計が考えられます。
次回は例外処理について解説します。