ちょっと昨日の記事を書いていて、プログラムサンプル作成に興が乗ったので、興はジャンプテーブルをC++で書いてみたいと思います。
まずはサンプルコード
#include < iostream > #include < array > #include < string > #include < algorithm > using namespace std; using CALLBACK = void (*)(); #define CHAR2NUM(character) ( reinterpret_cast流れを解説すれば、関数ポインタ型の配列に登録した関数を、Arrayコンテナの番号付き参照で直接呼び出して使用している。( character - '0') ) void test1(){ cout << "do U wanna call me? I'm 1 !" << endl; } void test2(){ cout << "do U wanna call me? I'm 2 !" << endl; } void test3(){ cout << "do U wanna call me? I'm 3 !" << endl; } void test4(){ cout << "do U wanna call me? I'm 4 !" << endl; } void test5(){ cout << "do U wanna call me? I'm 5 !" << endl; } void test6(){ cout << "do U wanna call me? I'm 6 !" << endl; } void test7(){ cout << "do U wanna call me? I'm 7 !" << endl; } void test8(){ cout << "do U wanna call me? I'm 8 !" << endl; } void test9(){ cout << "do U wanna call me? I'm 9 !" << endl; } void test0(){ cout << "do U wanna call me? I'm 0 !" << endl; } int main(int argc, char** argv) { array callback_array = { test0, test1, test2, test3, test4, test5, test6, test7, test8, test9 }; int call_num = 0; string input_buffer; cin >> input_buffer; for( auto it: input_buffer ) { call_num = CHAR2NUM( it ); if( ( call_num >= 0 && call_num <= 9 ) ) { callback_array.at(call_num)(); } } return 0; }
これを実行すると以下のような感じで実行結果が出る。
12345 do U wanna call me? I'm 1 ! do U wanna call me? I'm 2 ! do U wanna call me? I'm 3 ! do U wanna call me? I'm 4 ! do U wanna call me? I'm 5 !
細かい解説
関数ポインタ型の別名定義
using CALLBACK = void (*)();
関数ポインタは型としてエイリアスを持たせることができます。
これは当然の仕様で、ポインタはアドレスというよりも、そのアドレスにどのようなアクセスの仕方で読み出すか、という規約を記述したものだと理解すると分かりやすいです。
つまり uint32_t* 型であれば、格納されたアドレスを先頭に32bit(4byte)読み出し、その値を符号なし整数として扱う。
という意味になります。
関数ポインタ型の扱いも同様で、格納されたアドレスを先頭に関数呼び出しのエントリポイントとして扱う、という意味になります。
そう考えると普通のポインタと違いはないので、当然エイリアスを定義することができるわけです。
関数ポインタ型でコンテナを作成
arraycallback_array = { test0, test1, test2, test3, test4, test5, test6, test7, test8, test9 };
型として定義するということができるということは、テンプレートクラスの型指定子としても使用することができるというわけです。ここではstd::arrayコンテナを関数ポインタ型CALLBACK で10メンバ分定義しています。
基本的な使用方法は配列と変わらないので、添字アクセスが可能ですが、私の場合は境界値チェック機能付きのatアクセッサで使用するのが好きです。
これは、atアクセッサの境界チェックの仕組みが例外送出に依存する内容であるためです。atアクセッサに領域外のインデックスを設定すると、例外を創出します。これをtry-catchステートメントで受けて例外処理を行うことで、バッファオーバーフローによる被害を防ぐことができるわけです。
しかしtry-catchステートメントは、例外発生箇所からcatchステートメントの先頭まで処理が飛ばされる仕様上、性質が非常にgoto文に近いものとなっています。
開発メンバのリテラシーが確保されている場合は使用して良いですが、メンバーのレベルが揃わないのであれば原則として使用を制限するべきものであると考えています。
しかし同時にatアクセッサ自体が添字チェックの時点で例外を発布して、実際のアクセスそのものは防いでくれるため、範囲チェックを自前実装していたとしても原則的にアクセスに使用するべきは添字アクセスではなくatアクセッサ、ということになります。
C/C++はメモリアクセス周りの機能が非常に強力、というか強烈なので、些細なミスが致命的な結果に繋がらないよう、ミスを防ぐ作業ルールと被害を小規模に留めるテクニックを組み合わせ、安全なコーディングをすることがとても大事です。
関数ポインタ型を直接呼び出し
callback_array.at(call_num)();
c++のリファレンスには、以下の記述があります。
従来のCスタイルの配列のパフォーマンスを保ったまま、シーケンスのサイズの取得、要素の代入のサポートなど、標準コンテナの恩恵を受ける事ができる。このような性質のものですので、基本的にC++11以降のバージョンのC++を使用する場合は、配列ではなくArrayコンテナを使用するのが基本となります。
基本的な使用方法は配列と変わらないので、添字アクセスが可能ですが、私の場合は境界値チェック機能付きのatアクセッサで使用するのが好きです。
境界チェックは自前実装
ここで一点気付く方もおられる方がいらっしゃるかと思いますが、atアクセッサを使用しているにもかかわらず、境界チェックを自前で実装しています。これは、atアクセッサの境界チェックの仕組みが例外送出に依存する内容であるためです。atアクセッサに領域外のインデックスを設定すると、例外を創出します。これをtry-catchステートメントで受けて例外処理を行うことで、バッファオーバーフローによる被害を防ぐことができるわけです。
しかしtry-catchステートメントは、例外発生箇所からcatchステートメントの先頭まで処理が飛ばされる仕様上、性質が非常にgoto文に近いものとなっています。
開発メンバのリテラシーが確保されている場合は使用して良いですが、メンバーのレベルが揃わないのであれば原則として使用を制限するべきものであると考えています。
しかし同時にatアクセッサ自体が添字チェックの時点で例外を発布して、実際のアクセスそのものは防いでくれるため、範囲チェックを自前実装していたとしても原則的にアクセスに使用するべきは添字アクセスではなくatアクセッサ、ということになります。
C/C++はメモリアクセス周りの機能が非常に強力、というか強烈なので、些細なミスが致命的な結果に繋がらないよう、ミスを防ぐ作業ルールと被害を小規模に留めるテクニックを組み合わせ、安全なコーディングをすることがとても大事です。
文字列へのイテレータアクセス
for( auto it: input_buffer ) { call_num = CHAR2NUM( it );
旧来C/C++は文字列の扱いが苦手だと言われてきました。
それは他の後発言語と比べて文字列の処理を行う手段が配列しかなく、少々の処理を行うためにも膨大なコードを書かなければならないことに原因がありましたが、現在のC++はむしろ文字列の処理はかなりやりやすいものになっています。
stringクラスへのイテレータアクセスは、特にこのあたりを楽にしてくれています。
stringクラスの元となるbasic_stringクラスは、charT要素の文字列なので、イテレータを使用すれば一文字ずつを従来のC言語のように処理することができます。
また同時に、stringクラス自体も文字列を扱うためのオペレータや高度な検索、加工関数を多く備えています。
ここではC++11で追加された範囲for文と、昔ながらの文字コード加工マクロを使用することで、文字コードを配列の添字に加工してジャンプテーブルを実現しています。
リアルタイム性を求められる、ミッションクリティカルなプログラムでは、往々にして記述の容易さよりもこのような低層の情報を直接扱うことによる高速な処理が求められます。
そういう点から見ても、現在のC++のこのような仕様は、低層から高層までカバーできる柔軟性を備えており、非常に素性のいい仕組みであると言えます。
さいごに
今回はオーソドックスなジャンプテーブルを作成しましたが、これを応用することで非常に高度なコマンド解析も、条件分岐を使用することなく実装できてしまったりするので、高速なプログラミングを組んでいく一助になればと思います。
0 件のコメント:
コメントを投稿