B級科学者もどきの憂鬱

とある理系になりきれない奴のつれづれなる活動記

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

手間を省くライブラリとは?

今回は、プログラムのライブラリの設計について考えます。
ちなみに私は、ソフトウェア工学の専門家じゃないので、
プログラミング素人の戯言ってことで勘弁してください。
そんなわけで、カテゴリも考察じゃなくてプログラミングです。

プログラマの手間を省くためのライブラリというのは、
汎用的で、常に安全に使え、動作は十分に速く、
知識が付けば付くほどより効率的に組める、
そうあるべきものだと私は思っています。

この最後の項目については賛否あると思いますが、
今回はそれについて考えてみます。

私が作っているものの中から具体例を出してみます。
説明用なので、ごく一部のメソッドのみ書きます。
まだ自分の理想には遠いので、実装そのものは今は出しません。
いずれ出す時はあるかもしれませんが。

自作の高速フーリエ変換(FFT)クラスでは、
次のようなメソッドが用意されています。
なお、DataTypeというのは、このクラスの
テンプレート引数で指定される型名です。

bool Prepare(unsigned int length)
変換に際して用いられるテーブルを計算します。
テーブル作成に失敗したらtrueが返ります。

bool Transform(DataType data[], unsigned int length, bool isInverse=false)
変換処理を行う関数です。
順変換を行う場合は、isInverseにfalseを、
逆変換を行う場合は、isInverseにtrueを指定します。
変換に失敗したらtrueが返ります。

上の二つを使っていれば、とりあえず変換処理は組めます。
ただ、Prepareの呼び出しは必須ではありません。
Prepareを呼ばなかった場合、Transformでは、
計算テーブルを使わない、少し効率の悪いルーチンが使われます。

Prepareで指定したlengthとTransformで指定したlengthが
異なる場合も同じです。自動的にPrepareが呼ばれたりはしません。

lengthの値に応じて、Transformは、
FFTを処理するルーチンを自動的に選択します。
例えば、2の累乗値であれば、基数2のFFTが呼ばれます。

publicなメソッドは他にもあります。

void FFFT2Bmain(DataType data[], unsigned int length)
基数2のFFT順変換のメインルーチンです。

void IFFT2Bmain(DataType data[], unsigned int length)
基数2のFFT逆変換のメインルーチンです。

本来、FFTではビット反転という処理が必須なのですが、
この二つのメソッドは、内部でこの処理を行いません。
ある特別な条件下では、ビット反転が不要になる場合があるので、
わざわざpublicに用意してあります。

Transformでは、ビット反転処理とこのメインルーチンを呼び出しています。
しかし、特別な条件下、具体的には畳み込み演算などでは、
このメインルーチンのみで計算することが出来るので、
Transformを使うのに比べて、処理の無駄が省けます。

専門的な知識が付くと、より効率的なプログラムが組め、
知識が無くてもそれなりに高速なコードにはなる、
というのは分かって頂けるでしょうか。

私は、ライブラリというのは本来、
このようにして組むべきものだと思うのです。

専門的な知識が無いと使えない、さっきの例で言う、
FFTメインルーチンとビット反転メソッドだけのものでは、
そのライブラリを使おうとする際、予備知識が必要になります。
それでは、プログラマの手間があまり低減出来ていないと思うのです。

もちろん、より効率を上げようと思うのなら、
当然ながらプログラマ自身も勉強しないといけませんが、
効率はそれなりでいいから組めさえすればいいという人には、
出来る限り簡単な方法を提供すべきだと思います。

さて最後に、何でこんな記事を書いたのかというと、
DirectXのヘルプが分かりにくい上に専門用語だらけだからです。
専門用語を使うなとは言わないから、せめてその解説が欲しい……。

テンプレートの部分特殊化

TIPSエンジンのC++での組み直しで、
今は高速フーリエ変換の辺りをやっています。

メモリ使用量や計算量の削減のために、
実数型と複素数型で計算ルーチンを変えています。
その辺の関係で、私のコードでは今、テンプレートクラスが大活躍です。

細かい実装は省略しますが、概略はこんな感じ。

/*非複素数型向けの実装*/
template <class DataType=double>
class FFT
{
private:
FFTtable<DataType> table;

public:
bool Prepare(int size) {...}
bool Transform(DataType data[], int size, bool isInverse) {...}
FFT(void) {...}
virtual ~FFT(void) {...}
};

/*複素数型に対する部分特殊化*/
template <class DataType>
class FFT< std::complex<DataType> >
{
private:
FFTtable<DataType> table;

public:
bool Prepare(int size) {...}
bool Transform(std::complex<DataType> data[], int size, bool isInverse) {...}
FFT(void) {...}
virtual ~FFT(void) {...}
};

計算テーブルは型がどちらであろうと変わらないので、
全部別のクラス(FFTtable)にまとめてあります。

複素数には、STLのcomplexクラスを使いました。
いくつか前の記事でComplexクラスを簡易的に実装しましたが、
STLに同じものが存在することに気付いたのはあの後です……orz

計算の実装にはまだ多少無駄があるので、
これからもっと改良していく予定ですが、
とりあえずは動くので、これから次の所を組んでいきます。

後半の部分特殊化の構文ですが、
私はつい最近まで、こんな風に書ける事を知りませんでした。
ポインタ型に使える程度のものだと勝手に思っていたのですが、
もしかしてと調べてみて目的ドンピシャのものが見つかりました。
いやあ、疑ってみるものです。

ただ、個人的には、もっと様々な部分特殊化方法が欲しいです。
例えば、ある基本クラスを継承しているクラスのみに対する部分特殊化とか、
int型テンプレート引数がある数字以上ある数字以下である場合とか。

後者は、C++の文法にちょっと手を加えれば出来そうな気がします。
最近、C++0xの仕様が確定して名前が変わったそうですが、
是非こういう機能を次のバージョンに盛り込んでほしいものです。

適切なメモリ管理方法は?

私は今まで、PCゲームは一度も作ったことがなかったのですが、
何故か、諸事情でDirectXでゲームを作る羽目になりました。
その関係でちょっとまた忙しくなっています。
と言っても、前ほどではありませんが。

今回はそれに関連した、メモリ管理方法の話です。
正直私も大したことは知らないので、
もっといい方法を知っている方は教えて下さると泣いて喜びます。

ゲームに限らず、大量の画像や音楽ファイルを扱うソフトは、
メモリ管理がかなり重要になりますよね。
特に、低スペックパソコンでも動くようにしたい場合は重要です。

無駄を省くため、必要のあるものだけを確保し、
いらなくなれば即座に解放したいところですが、
明示的にnew deleteを書くだけのプログラムは、
かなり危険なので出来る限り避けるべきだとは思います。

ある所でメモリが確保されたら、
明示的に解放しても大丈夫で、解放を忘れても
システムが自動で解放してくれるのが理想的です。
それをやってくれるのがガベージコレクタですが、
通常のC++にはそれはありません。

とりあえず私は次善の策として、
解放の必要なものは色々とクラス化して、
明示的に解放するのはもちろん、解放を忘れても、
デストラクタで勝手に解放してくれるようにしました。

毎度毎度クラスを作るのは多少面倒ですが、
クラスをnewで確保せずに通常の変数として宣言すれば、
寿命が来れば勝手にデストラクタが動くので、解放忘れはなくなります。

一応これである程度上手く実装できたと思うのですが、
本来こういう場合のメモリ管理って、
どんなふうに書くのがセオリーなんでしょうか?
何か代表的な手法とかあれば是非知りたいです。

一応今後の方向としては、メモリ不足が検出された場合、
自動的に解放されるような仕組みも作ろうと思っています。

そのために、メモリ管理クラスを実装していこうと思います。
このクラスはDirectXに限らず汎用的に使えるようにしておけば、
音声合成関係など、他の開発にも使えそうですね。
まぁこんなことやってるからプログラムが進まないんですが。

本当はガベージコレクションを実装して、
メモリ領域を効率的に使えるようにもしたいのですが、
テクスチャ用メモリの確保などはDirectXの関数が内部で行うため、
確保された領域のアドレスがわかりません。
なので、不可能ではないかも知れませんが、かなり困難なので断念します。

群を継承して実装したい

最近だいぶ忙しさが無くなってきたので、
TIPSエンジンをC++で書き直しています。
で、どうせなら汎用的なライブラリにしてしまおうと、
色々と定義していたら、いつの間にか複素数クラスを作っていました。

より良い物を求めるのはいいことだとは思いますが、
いくら何でも基礎からやり過ぎのような気がします。
でもまぁ今作っておけば、今後の開発にも役立つでしょう。多分。

どうせなら多倍精度小数や四元数も欲しいと思ったので、
複素数クラスのメンバ関数をうまく利用して作れないかと考えたのですが、
静的型付け言語では中々難しいようですね。

具体的には、メンバを持たず、演算だけを宣言したテンプレートクラスを用意し、
それを多重継承していくことで、クラスの性質だけを定義しようと考えました。
まぁつまり、代数的構造(群など)をインターフェースにして、
それを継承することでクラスの性質を定義しようというものです。

コードとしてはこんな感じです。


/*加算を演算とする群インターフェース*/
template <typename ValueType>
struct IAddGroup
{
virtual ValueType operator+(const ValueType&) const=0;
virtual ValueType& operator+=(const ValueType&)=0;
virtual ValueType operator-(const ValueType&) const=0;
virtual ValueType& operator-=(const ValueType&)=0;
virtual ValueType operator-() const=0; /*逆元を計算*/
virtual ValueType GetZero() const=0; /*単位元を取得*/
virtual ~IAddGroup(){};
};

/*複素数クラス 乗算なども必要だが今回は省略*/
template <typename ValueType>
struct Complex :
public AddGroup< Complex<ValueType> >
{
ValueType r, i; /*実部と虚部*/

virtual ValueType operator+(const ValueType& value) const {
this->r=value.r;
this->i=value.i;
}
virtual ValueType& operator+=(const ValueType& value) {
return *this=*this+value;
}
/*減算などの実装も、上と同じように出来るので省略*/

/*コンストラクタ等も省略*/
};


群インターフェースには実装はありません。
演算のみが純粋仮想関数で宣言されています。
それをComplexクラスで継承することで、
強制的に演算を実装させることが出来ます。

こうして、群の性質を継承したクラスを作れる訳ですが、
本来群というのは、演算方法(仮に+とします)、逆元の取り方、
そして単位元の三つが分かれば、+=、-、-=の演算は、
全て実装出来るはずですよね。

ということは、群インターフェースではなく群クラスにして、
演算のいくつかだけを純粋仮想関数にするだけでも良さそうなものです。
つまり、Complexクラスのoperator+=などの実装は、
IAddGroupに書いてしまいたいのですが、うまく行きません。

まぁそりゃそうです。もしIAddGroupにComplexの+=の実装を書くと、
thisポインタはIAddGroupを示すので、演算するためのメンバが無くなります。
関数オブジェクトを用いるなど、実現自体は一応可能なのですが、
もっと単純に、出来れば継承元にメンバを持たせずに実現したいところです。

このために色々考えてはみたのですが、上手いアイデアが思いつきませんでした。
現段階でも、性質の継承という目的は一応果たせていますし、
継承先では+と+=でそれぞれコードを最適化した方が大抵速いはずなので、
とりあえずこれで開発していくつもりです。

でもやっぱり気になるのは事実。
どなたか良い方法を知りませんか?

スタイルの指針

最近友人に、Javaを教えてくれ! と頼まれました。
オブジェクト指向の諸々とかそういうのではなく、
主に作っているアプリのバグ取りが中心なのですが、
友人も学び始めらしいので適当に同時に教えてます。
で、そこでちょっと気になったこと。

よくプログラミング界隈で議論になる事ですけど、
皆さんは、中括弧「{}」ってなるべく省略しますか?
その友人は、省略できる時は常に省略する人なのですが、
私は、省略する場合は必ず制御文と同じ行に書くようにしてます。

よく議題になるプログラムスタイルは他にも色々ありますが、
私は一応、間違いに気付きやすいかと、
単純に美しいかという二つの基準で決めています。
優先度はもちろん前者の方が上ですが。

C言語で例を挙げてみます。
変数と直値を等価演算子で比較する場合、

if(pointer==NULL)

のように、左に変数、右に直値ではなく、

if(NULL==pointer)

と逆にします。このとき、「==」をもし「=」と間違えた場合、
(VBなんかと交互にコーディングしてるとよくある)
文法エラーが確実に起こり、ミスが発見できます。

プログラムを色々書いたり読んだりしていると、
どんなバグが出やすいかがわかってきます。
で、その書き方でそのバグが回避できるのなら、
出来る限りそのスタイルでやりたいよねっていうことです。

プログラムは確かに文学のような部分もありますけど、
やっぱり『厳密な手順書』という意味が一番ですし、
何であっても、変更が容易で理解しやすいというのに
越したことはありませんよね。

FC2Ad

まとめ

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。