ADPの開発が滞っていますが、思わぬところで話が進んでしまい、今年最後の記事になります。
社会人であり、技術者でありのコメント欄で結合について話が盛り上がったのですが、『私がHitAndBlowを作ったらどんなコードになるか?』ということで作成してみました。Visual C++ 2008で動作確認しました。
επιστημηさんの真似だと芸がないので、出題者・回答者をそれぞれ人間・コンピュータから選べるようにしました(大掃除をさぼったので嫁に怒られながら作りました・・・)。
それにしてもC++のコードは人によって個性が出ますね。
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <math.h>
#include <ctype.h>
#include <time.h>
using namespace std;
class HABReferee {
vector<int> answer;
vector<bool> blowtable;
public:
bool prepareAnswer(const vector<int> &answer_) {
blowtable.assign(10, false);
for ( size_t i = 0; i < answer_.size(); i++ ) {
if ( blowtable[answer_[i]] ) {
return false;
}
blowtable[answer_[i]] = true;
}
answer = answer_;
return true;
}
bool submitAnswer( const vector<int> submit, int &hit, int &blow) {
if ( answer.size() != submit.size() ) return false;
hit = 0;
blow = 0;
for ( size_t i = 0; i < submit.size(); i++ ) {
if ( answer[i] == submit[i] ) {
hit++;
} else if ( blowtable[submit[i]] ) {
blow++;
}
}
return hit == submit.size();
}
};
static vector<int> inputNumbers(int N) { // N桁の数値の入力を行う(違った場合はやり直し)
vector<int> result;
string str;
do {
result.clear();
cin >> str;
for ( size_t i = 0; i < str.size(); i++ ) {
if ( isdigit(str[i]) && str[i] != '0' )
result.push_back(str[i] - '0');
else
break;
}
} while ( result.size() != N );
return result;
}
class HABContributor { // 出題者(人間)
public:
virtual vector<int> prepareAnswser(int N) {
cout << "各桁が1~9である" << N << "桁の数を入力してほしい。各桁で数が重複するのは避けてくれ" << endl;
return inputNumbers(N);
}
};
class HABContriburerComputer : public HABContributor { // 出題者(乱数生成)
public:
virtual vector<int> prepareAnswser(int N) {
vector<int> digits;
for ( int i = 1; i < 10; i++ ) {
digits.push_back(i);
}
vector<int> result;
srand((unsigned int)time(0));
for ( int i = 0; i < N; i++ ) {
vector<int>::iterator itor = digits.begin() + rand() % digits.size();
result.push_back(*itor);
digits.erase(itor);
}
return result;
}
};
class HABSolver { // 回答者(人間)
public:
virtual void prepare(int N) {}
virtual vector<int> getAnswer(int N) {
cout << "答えを予想してくれ" << N << "桁の数だ。" << endl;
return inputNumbers(N);
}
virtual void giveHint( int hit, int blow) {
cout << hit << "Hit" << " / " << blow << "blow" << endl;
}
};
class HABSolverComputer : public HABSolver { // 回答者(コンピューター)
HABReferee checker;
vector<vector<int>> candidate;
public:
void recur(vector<int> &answer, int N) {
if ( N == 0 ) {
candidate.push_back(answer);
} else {
for ( int i = 1; i < 10; i++ ) {
if ( find( answer.begin(), answer.end(), i) == answer.end() ) {
answer.push_back(i);
recur( answer, N-1);
answer.pop_back();
}
}
}
}
virtual void prepare(int N) {
vector<int> answer;
recur( answer, N);
}
virtual vector<int> getAnswer(int N) {
cout << "答えは";
for ( int i = 0; i < N; i++ ) {
cout << candidate.back()[i];
}
cout << "かな?";
return candidate.back();
}
virtual void giveHint( int hit, int blow) {
HABSolver::giveHint( hit, blow);
checker.prepareAnswer(candidate.back());
for ( vector<vector<int>>::iterator i = candidate.begin(); i < candidate.end(); ) {
int ahit, ablow;
checker.submitAnswer( *i, ahit, ablow);
if ( ahit != hit || ablow != blow ) {
i = candidate.erase(i);
} else {
i++;
}
}
}
};
class HABGame {
int N;
HABReferee referee;
HABContributor *c;
HABSolver *s;
public:
HABGame(int N_, HABContributor *c_, HABSolver *s_) : N(N_), c(c_), s(s_) {};
void play() {
// 出題者から問題をもらいレフリーに渡す。
while ( !referee.prepareAnswer(c->prepareAnswser(N)) )
; // 規格にあったものが出てくるまでループする
bool endflag = false;
int hit;
int blow;
s->prepare(N); // 回答者に準備をさせる
while ( endflag == false ) {
// 回答者から回答をもらい判定する。
endflag = referee.submitAnswer( s->getAnswer(N), hit, blow);
// 回答者にヒントを言う。
s->giveHint( hit, blow);
}
}
};
int main()
{
HABContributor hc;
HABContriburerComputer cc;
HABSolver hs;
HABSolverComputer cs;
HABContributor *c;
HABSolver *s;
string str;
while ( true ) {
cout << "メニュー" << endl
<< "1:出題者(Human) vs 回答者(Human)" << endl
<< "2:出題者(Human) vs 回答者(Computer)" << endl
<< "3:出題者(Computer) vs 回答者(Human)" << endl
<< "4:出題者(Computer) vs 回答者(Computer)" << endl
<< "0:終了" << endl;
cin >> str;
switch( str[0] ) {
case '0' :
return 0;
case '1' :
c = &hc;
s = &hs;
break;
case '2' :
c = &hc;
s = &cs;
break;
case '3' :
c = &cc;
s = &hs;
break;
case '4' :
c = &cc;
s = &cs;
break;
}
HABGame g(3, c, s);
g.play();
}
return 0;
}
ちょっと余計な記事が入りましたが、続きを
C++の仮想関数の欠点
話が少し前後しますが、Part4の記事でC++の仮想関数呼び出しの仕組みについて説明しましが、ここではC++の仮想関数の欠点について指摘します。C++ではvtableというメンバ関数のアドレスを集めたテーブルを用いて仮想関数の呼び出しを実現していました。この方式は効率がよいのですが『コンパイル時に呼び出すべき仮想関数が決定しなければならない』という弱点があります。
どういうことかといいますとC++でのメンバ関数呼び出し
object.virtual_method( arg1, arg2, arg3)
という呼び出しで、virtual_methodというメンバ関数名はコンパイル時に参照されますが、実行時には内部的に振られた番号(vtableのインデックス)になります。つまり実行時にはこの名前は参照できません。と同時にvtableのインデックスを取得する手段もないので、実行時に呼び出すメンバ関数を選択したいということができません。
これの何が欠点かピンとこないかもしれませんが、例えば、バッチファイルからVBScriptを使ってExcelを操ったりしますが、この特にExcelのバージョンをあまり気にせずにExcelを操作(メソッドを呼び出す)するでしょう。これと同じことは、C++の仮想関数の仕組みではストレートに実装できないということです。Windowsでは皆さんご存知のとおり、COMという仕組みをOSに実装することで実行時に呼び出すメソッドを特定することを行っています。
COMというとえらく古いと思われるかもしれませんが、.NET Framkework からExcelを呼び出す場合もCOM相互運用性という仕組みを使って.NET Framework → COM → Excel という風に呼び出しいます。
話が脱線しますが、私は.NET Frameworkが廃れるのではないか? と思っていますが、その理由のひとつが .NET FrameworkがCOMやOLE DB等のようにWindows APIを充分に置き換えていないと思えるところにあります(もっとも先のことは解りませんのでなんともいえませんが)。
関数の動的なロード&実行の例
場合によって呼び出す関数を変える
というプログラミングテクニックは、オブジェクト指向プログラミング以外にもあります。典型的な例のひとつにデバイスドライバがあります。
デバイスドライバはご存知のとおりハードウェアとOSのAPIを橋渡しするソフトウェアでハードウェアに合わせて作成されています。ハードウェアを変えるとそれにあわせてデバイスドライバも変えます。
デバイスドライバはCで記述されることが多いです。最近のOSではPlag&Playが一般的になりましたし、USB接続機器ではOSを再起動せずに、デバイスドライバがロードされます。このような動的なソフトウェアのロードの仕組みはどうなっているのでしょうか?
続いては、公開1周年記念特集記事として『プログラミング言語の制御構造のいろいろ(5)』を書いてみます。
Part3の記事が短く、Part4(前回の記事)が長かったりバランスが悪いですが、まぁBlogということでご容赦を。
メンバ関数呼び出し(thiscall)の補足
前回の記事にありましたメンバ関数の呼び出し規約(thiscall)について少し補足しますと、この呼び出し方法は一部のコンパイルで採用されているもので全てのコンパイラに当てはまりません。ちなみにVisual Studio 2008のC++コンパイラも違うやり方を採用しており、thisポインタをスタックに積むのではなくECXレジスタに代入します。thisポインタをECXに保存するとメンバ変数にアクセスする際に高速に処理が行えるのでこの方が効率的かと思います。
思い出話をしますと、Visual Stduioの昔のバージョンでは、thisポインタはスタックに積まれていたと記憶しています。ADPの高速化に際してアセンブラコードを読んでいて『なんか変だな・・・』という感じで調べるとVisual Stduio 2008ではこのようになっていると気づきました。
仮想関数とif文
前回の記事に「仮想関数の説明はしない」と書きましたが、よくよく考えると仮想関数の仕組み(というか利用方法)を書かないと言いたいことが言えないことに気づきましたので書きます。
仮想関数の有効性を示す例を示します。
以下、C/C++の擬似コードになります。if文を用いてデータタイプを判定しデータタイプに応じた変換を行い、valueの内容を文字列に変換しています。
char buf[512];
if ( value_type == int ) {
sprintf( buf, "%d", value);
} else if ( value_type == double ) {
sprintf( buf, "%f", value);
} else if ( value_type == char* ) {
strcpy( buf, value);
}
ここで、個別の変換処理(sprintfやらstrcpy)を仮想関数に置き換えます。
virtual int::to_string(char *buf) {
sprintf( buf, "%d", value);
}
virtual double::to_string(char *buf) {
sprintf( buf, "%f", value);
}
virtual char*::to_string(char *buf) {
strcpy( buf, value);
}
呼び出し部分のコードは、以下のとおりになります。
char buf[512];
value.to_string(buf);
仮想関数とはデータのタイプに合わせて実際のメンバ関数が呼び出される言語の機能になります。ここで、to_stringが仮想関数になり、実際に呼び出されるメンバ関数は、valueのデータタイプ(intやらchar*)に合わせて呼び出されることになります。ちなみにC++ではintやdoubleはクラスではないのでこのような仮想関数を作成することは出来ませんのであくまでも例になります。
仮想関数ですが、一見ややこしいですが、『データタイプに合わせて処理を行う』ような場合にはほぼ問題なく仮想関数に変換できるかと思います。ADPはC++で作成していますが、多くの場面で仮想関数を使っています。
例ではかなり端折っているので便利さが伝わりにくいですが、仮想関数を用いるとswitch文が減る(switch文もif文の一種と考えられる)といわれているとおり、今までif文が連なっていたコードが
value.to_string
とすっきりと記述出来るようになっています。重要な点はif文で書かれたコードブロックが to_string に置き換わり抽象度が上っていることです。抽象度が上がることが必ずしも可読性が増すわけではないですが、仮想関数を用いると呼び出し側のコードがすっきりとすることは分かるかと思います。
本記事のテーマである制御構造のいろいろという観点でみますと、
仮想関数とはif文と関数呼び出しが混ざったものとも理解できます。
ちなみに、オブジェクト指向というとどうしても大きな括り(動物クラスとか社員クラスとか)の話になりますが、int型とかdouble型のような基本的な型でも行うことが出来、結構便利だったりします。C++では、intやdoube型ではメンバ関数を作ることが出来ませんが、Rubyのように使える言語もありますので試す価値はあります。またADPも同様なコードを記述することができます。
ADPのユニフィケーション(パターンマッチング)
前節で、仮想関数はif文と関数が混ざったものと説明しましたが、ADPでは関数呼び出し(述語の評価)に際しては、まずユニフィケーションが行われます。これは仮想関数を一般化しより強力かつ柔軟に呼び出すべき関数を選定できると考えることも出来ます。
ユニフィケーションについては、
こちらを参照下さい。
続いては、公開1周年記念特集記事として『プログラミング言語の制御構造のいろいろ(4)』を書いてみます。
前回からちょっと間が空いてしまいましたが、ADPの1周年記念記事のPart4です。
関数呼び出しのスタックの使われ方
前回の記事の終わりにスタックという言葉が出てきましたが、スタックとはプロセス(正確にはスレッド)毎に用意されているメモリエリアで、関数呼び出しやローカル変数の保持に使われます。
以下のC言語での関数呼び出し時のスタックの使われ方の例を図1に示します。
func( arg1, arg2, arg3); /* ------- ※1 */
図1
スタックは伝統的にアドレスの上位(数字が大きい)から下位に向かって領域が確保されます。
※1の関数が呼び出されるとき、先ず引数がスタックに積まれ、次いでリターンアドレス、そしてローカル変数の領域が確保されます。関数というのはどこから呼び出されても元の場所に戻ることが出来ますが、それが実現できるのは、呼び出し後に実行すべき命令のアドレス(リターンアドレス)をスタックに保持しているからです。
また、同時にどこから呼び出されてもローカル変数が『関数内で一時的に有効な変数』として機能できるのもスタックに変数のエリアを確保しているからになります。
ちなみに、数年前に流行したセキュリティリスクでバッファオーバーランというものがありますが、これはローカル変数の領域を溢れさせアドレスの上位にある戻りアドレスを書き換えてウイルスのプログラムを実行しようというC言語の関数呼び出しの仕組みを悪用したものになります。現在ではCPUレベルでの対策(NXビットとかXDビットとか呼ばれものでデータ領域の実行の禁止)が行われ、バッファオーバーランの脆弱性が起こりにくくなっています。
スタックには引数が積まれていますが、引数が積まれる順番には2通りのやり方があります。図1ではリターンアドレスに次いで arg1,arg2,arg3 と積まれていますが、反対に arg3,arg2,arg1 というやり方もあります。arg3,arg2,arg1の順番ですが、一見すると反対に見えますが、スタックに積む順番はarg1,arg2,arg3となります。ややこしいですが、※1の擬似アセンブラコードを示すと意味が良く分かるかと思います。
※2 ※1の擬似アセンブラコード(cdecl呼び出し)
PUSH arg3
PUSH arg2
PUSH arg1
CALL func
PUSH命令の発行順とスタック上のリターンアドレスから見た順番が反対になります。
関数の呼び出し方法(つまりどのように機械語に翻訳するか)を呼び出し規約(主にx86のCPUで用いられている表現)といい、※2のような呼び出し方法をcdeclと呼びます。呼び出し規約はその他にPASCAL(文字通りPASCALで採用されている)とかstdcall(Windows-APIで採用)とかthiscall(C++のメンバ関数呼び出し)等があります。
メンバ関数の呼び出しでのスタックの使われ方
続いて、C++のメンバ関数呼び出しでのスタックの使われ方について説明します。
以下のC++でのメンバ関数の呼び出し時のスタックの使われ方の例を図2に示します。
object.method( arg1, arg2, arg3); // ------- ※3
図2
※3の擬似アセンブラコードを以下に示します。
※4 ※3の擬似アセンブラコード(thiscall呼び出し)
PUSH arg3
PUSH arg2
PUSH arg1
PUSH object
CALL method
違いは、object(正確にはobjectのアドレス)がthisポインタとして引数の一つとしてスタックに積まれていることです。その他の違いはありません。こうしてみるとオブジェクト指向というのは単純に
method( &object, arg1, arg2, arg3)
というコードを、
object.method( arg1, arg2, arg3)
という風に記述できる構文上の違いであるに過ぎないということに気づくかと思います。
ADPでは、この考え方を推し進めて、メソッド形式(メンバ関数呼び出しとほぼ同じ意味)として通常の述語形式での呼び出しとメソッドの呼び出しを混ぜて使うことができるようにしています。
ちなみに、私も含めて、多くのC言語の上級エンジニアがこのような見方をしてC言語からC++(オブジェクト指向)に移行していたかと思います。
もっとも、この話は、『仮想関数はどのように機械語に翻訳されるのか?』の話をしなければ終わりになりません。
次いで、仮想関数の呼び出しの話をします。
仮想関数の呼び出しでのスタックの使われ方
前節で説明したメンバ関数の呼び出しは従来の関数呼び出しの延長線上のものですが、ここでは、仮想関数と呼ばれるオブジェクト指向独特の呼び出し方法について説明します。
ちなみに仮想関数の説明自体は省略します(コメント欄でリクエストを頂ければ記事を追加するかもしれません)。 仮想関数の説明は次の記事で行います。
以下の仮想関数の呼び出しについて考えます。ちなみにスタックの構成は図2で仮想関数・通常のメンバ関数(非仮想関数)での違いはありません。
object.virtual_method( arg1, arg2, arg3); // ------- ※5
※5の擬似C++コードを以下に示します。
※6 ※5の擬似アセンブラコード(thiscall呼び出し)
PUSH arg3
PUSH arg2
PUSH arg1
PUSH object
MOV EAX, [object + vptr] ; ------------------- A
MOV EDX, [EAX + virtual_method_offset] ; ----- B
CALL EDX ; ------------------------------------ C
object + vptrなどや、EAX + virtual_method_number の部分がかなり曖昧ですが、エッセンスとして読んでいただければと思います。
※6のアセンブラコードではよく分からないかと思いますので、まずはオブジェクトのメモリレイアウトを図3に示します。
図3
vtableと呼ばれるテーブルに呼び出すべき仮想関数の場所(アドレス)が格納されています。
また各objectはvtableの場所(アドレス)を保持する変数(ポインタ)を持っています。
さらに、機械語の特徴のとして関数呼び出し(CALL命令)は、常に同じ場所(アドレス)の関数を呼び出すだけでなく、変数(レジスタ)を通して間接的に呼び出すこともできるようになっています。
以上を踏まえて再度、擬似アセンブラコードを説明しますと、
Aでは、vtableを参照しています。EAXとはレジスタというCPUが持っている変数になりますがそこへvtableのアドレス(vptr)を代入しています。[] というのはアセンブラでのポインタ参照(間接演算子 *)になります。
Bでは、virtual_methodの呼び出すべきアドレスを、EDXに代入します。このvirtual_method_offsetですが配列のインデックスのようなもので、図3では0ということになります。
最後のCのCALL命令が、A,Bを通して取得した呼び出すべき仮想関数の呼び出しを行っていることになります。
このように擬似アセンブラコードを通してみますと、説明は難しいですが、たったの2命令の追加で仮想関数呼び出しを実現しており、C++での仮想関数呼び出しというのはかなり効率的であることが分かります。
もともと、私はアセンブラが大好き(ハードウェアを直接制御できるので)だったのですが、時代に押されてC言語を使うようになりましたが、その理由の一つとしてC言語が高級アセンブラとして設計された(つまりこのように簡単にアセンブラに置き換えられる)から動作がよく理解しやすい面があったからで、その設計思想はC++にも引き継がれていることが分かります。
続いては、公開1周年記念特集記事として『プログラミング言語の制御構造のいろいろ(3)』を書いてみます。
ADPの1周年記念特集のPart3です。『プログラミング言語の制御構造のいろいろ』ということで数回にわたって記事をアップします。ちなみに本日でちょうどADPの初回リリースから1年になります。
「なぜ、制御構造?」と思われるかもしれませんが、それはADP(Prolog)が持っている制御構造(バックトラック)が独特のものということと、JavaScriptやRubyにありますクロージャが本格的に普及してきて私自身が持っている制御構造に対する考え方(というか感覚)を変える必要があるので記事にしてみます。
制御構造とは
制御構造とはプログラムの流れ、広くはその命令(for文とかif文)を指します。制御構造を有名なものにしたのは、かのダイクストラ氏が提唱した構造化プログラミングがあります。今となっては『構造化プログラミング』という言葉を始めて聞いた人もいらっしゃるかと思いますが、『構造化プログラミング』が提唱された後に、今ではおなじみの制御構造文
・選択(if)
・反復(for,while等のループ)
が明確になりました。それまでの言語ではif文やfor文もありましたが充分でなく、本格的なプログラムの記述にはgoto文を使う必要がありました。そのれに加えてgoto文では様々なプログラムの流れを作ることが出来、流れの追いにくいいわゆるスパゲティプログラムというものもありました。私が駆け出しの頃(20年程前)にはよく可読性の悪いプログラムに対して『このスパゲティプログラムが~』という表現を聞いていました。
機械語ではどうしているのか?
なぜ、「機械語の話が出てくるのか?」と思われるかもしれませんが、制御構造の発展の歴史のルーツを探ることと、コンパイラ言語では制御構造が機械語に変換されるのでその仕組みを探るという意味で、続いて機械語の話をします。
機械語では初期のプログラミング言語のように比較文(if文)とgoto文のみで制御を行います。今となっては逆に難しいかもしれませんが、for文やwhile文がなくてもif文とgoto文の組み合わせでループを記述することが出来ます。
意外に思われるかもしれませんが、もう一つの制御構造文である関数呼び出し(サブルーチン呼び出し)も機械語にCALL命令という形で存在します。初期のCPUにはCALL命令がないものもあったらしいですが、今われわれが主に使っているパソコンのx86と呼ばれるCPUにもCALL命令があります。さらにx86の先祖をたどりますと、8080というパソコン用の8ビットCPUがありますが、そのCPUにもCALL命令があります(それから先は8008、4004とたどれますがこれらにCALL命令があるかどうかは不明です・・・)。
もちろんCALL命令が関数呼び出しとイコールではありません。CALL命令と関数呼び出しの違いは引数の受け渡しになります。CALL命令には引数の概念がありません。引数の受け渡しはレジスタまたはスタックまたはグローバル変数ということになります。C言語の関数呼び出しが機械語に翻訳されるるとCALL命令に翻訳されますが、その引数はスタックで渡されます。
続いては、公開1周年記念特集記事として『プログラミング言語の制御構造のいろいろ(2)』を書いてみます。