C plus plus テンプレートの代用としての疑似テンプレート使用
Mykola Demko | 29 10月, 2015
はじめに
言語の標準としてテンプレートを実装することに関する質問は mql5.comフォーラムでよく出ます。MQL5開発で拒絶の壁に直面すると、カスタムメソッドを使ったテンプレート実装についての興味が膨らむのです。というわけで私の調査結果を本稿で提供していきます。
C 言語と C++言語の歴史
当初より C 言語はシステムタスクを処理する機能を提供するために開発されました。C 言語製作者は言語の次項環境の中小モデルは実装しませんでした。システムのプログラマーが必要とする機能を実装したにすぎません。まず、それらはメモリを使用した直接動作、コントロールのストラクチャ構築、アプリケーションのモジュール管理のメソッドです。
実際、言語にはそれ以外なにもインクルードされていません。その他はすべてランタイムライブラリに取り込まれていました。C 言語をストラクチャされたアセンブラと呼ぶたちの悪い人達がときどきいるのはそういう理由です。その人達が何を言おうが、手法は大成功を収めているように見えました。そのようにして、簡素化と言語力の間の割合は新たなレベルへ達したのです。
よって、C 言語はシステムプログラムの汎用言語のようなものです。しかし、それはこれら限界に留まりませんでした。80年代、Fortran をリーダーの座から追いやり、C 言語は世界中のプログラマーの間で高い人気を博しました。そして異なるアプリケーションで広く使われるようになったのです。その人気へ大きく貢献したのは大学でUnix (また C言語も)を配布したことです。そこではプログラマーの新世代が教育を受けていました。
すべてが明白なら、別の言語すべてはいまだに使われ、それが存在できる理由はなんでしょうか?C 言語のアキレスのかかとは90年代に起こった問題に対してレベルが低すぎることです。この問題には2つの局面があります。
まず、言語レベルが低すぎるというのは、それはメモリとアドレス幾何の作業だということです。プロセッサのビット変化は多くの C言語アプリケーションに多大な問題を起こします。一方で、高レベルの不在は、データとオブジェクトのC 抽象タイプにおいては、多相性と例外処理を意味します。よって、C 言語アプリケーションではタスク実装技術がしばしばマテリアル面を越えてしまうのです。
こういった欠点の第一の修正は08年代初頭に試みられました。その頃、AT&T Bell 研究所のBjarne Stroustrupが「クラスを持つC 」と呼ぎ C言語の拡張版を開発しました。その開発スタイルは C 言語そのものの作成姿勢と変わるところはありませんでした。特定グループの人の作業がしやすくなるような異なる機能が追加されたのです。
C++ 言語における主要な改善は新しいタイプのデータを使用する可能性を与えるクラスのメカニズムです。プログラマーはクラスオブジェクトの内部表現と関数メソッドセットを記述しその代表にアクセスするのです。C++ 言語製作目的のひとつはすでに書かれたコードを再利用する割合を増やすことでした。
C++ 言語の改善はクラスのイントロ部分だけを構成するのではありません。例外を構成的に処理するメカニズム(それがないfail-safeアプリケーションの開発は複雑なものとなります。)、テンプレートメカニズム、そのた多くのものを実装しました。このため、言語開発の主要部分はANSI Сとのフルな互換性は保存したまま、新たな高レベル構成を導入することでその機能を拡げるように、との指示でした。
マクロ代用のメカニズムとしてのテンプレート
MQL5でテンプレートを実装する方法を理解するには、それがC++言語でどのように動作するか理解する必要があります。
定義を確認します。
MQL5 はテンプレートを持ちません。だからといってテンプレートありのプログラムスタイルが使用できないわけではありません。C++ 言語におけるテンプレートのメカニズムは、実際言語の深く埋め込まれたマクロ生成の洗練されたメカニズムです。言い換えれば、プログラマーがテンプレートを使用するとき、コンパイらが、宣言されるデータタイプではなく対応する関数が呼ばれるデータタイプを判断します。
プログラマーによって書かれたコードの量を減らすように C++ 言語で紹介されたテンプレートです。しかし、プログラマによってキーボードでタイプされるコードはコンパイラによって作成されるコードとは異なります。テンプレート自体のメカニズムはプログラムサイズを減らす結果にはなりませんでした。ただソースコードのサイズを減らしただけでした。それがテンプレートを用いて解決する主要な問題がプログラマーによってタイプされるコードを減らすことな理由です。
マシーンコードはコンパイル中に生成されるため、通常のプログラマーは関数コードが一度だけ作成されているのか複数回作成されているのか確認できないのです。テンプレートコードをコンパイルする間、関数のコードはタイプ数と同じだけ生成されます。そこではテンプレートが使用されます。基本的にテンプレートはコンパイルの段階でオーバーライドされます。
C++ 言語でテンプレートを導入する二番目の側面はメモリアロケーションです。C 言語でのメモリ問題は静的に割り当てられます。この割当てをより柔軟にするために配列メモリサイズを設定するテンプレートが使われます。ただ、この側面はすでに MQL4 で動的配列としてすでに実装ずみで、また MQL5 でも動的オブジェクトとして実装されています。
よって未解決事項はタイプ代用問題だけです。MQL5 開発者はそれを解決しようとしませんでした。テンプレート代用メカニズムの使用を参照することでコンパイラーを破壊し、そのため再コンパイルとなりそうだというのです。
彼らはそれをよく承知しています。そこで、選択肢はひとつしかありません。このパラダイムを独自の方法で実装するのです。
まず、我々はコンパイラーを変えることも、言語基準を変えることもないことをお伝えします。私が提案するのはテンプレート自体へのアプローチを変えることです。コンパイル段階でテンプレートを作成できなくても、それでマシンコードを書くことができなくなるわけではありません。私の提案は、バイナリコードの部分的作成からテキストコードが書かれる部分にテンプレートの使用を移動することです。この手法を「疑似テンプレート法」と呼びましょう。
疑似テンプレート法
疑似テンプレートにもC++ 言語テンプレートと比較するとメリット、デメリットがあります。ファイル移動の余計な操作が必要なのはデメリットです。言語基準に縛られず柔軟なことはメリットの一つです。では、言葉から行動に移ります。
疑似テンプレートを使用するのに必要なことは、プロセッサの類似体です。そのため、スクリプト「テンプレート」を使用します。スクリプトに対する一般的要件があります。スクリプトは指定のファイル(データストラクチャを持つ)を読むこと。また、テンプレートをみつけ指定タイプに置き換えることす。
ここで一言必要なようです。テンプレートの代わりにオーバーライドメカニズムを利用しようとしているので。コードはオーバーライドされるタイプ数だけ書かれることとなります。要するに、代用は分析に与えられるコードすべてを処理するのです。よって、コードはスクリプトによって複数回書かれ、毎回新規代用が作成されるのです。「機械による手動作業」スローガンを思い出します。
スクリプトコード作成
要求されるインプット変数を決めます。
- 処理されるファイル名
- オーバーライドされるデータタイプを格納する変数
- 実データタイプの代わりに使用されるテンプレート名
input string folder="Example templat";//name of file for processing input string type="long;double;datetime;string" ;//names of custom types, separator ";" string TEMPLAT="_XXX_";// template name
コードの一部だけにスクリプトを繰り返すには、マーカー名を設定します。マーケット開始は処理開始を指示し、終了マーカーは終了を指示します。
スクリプト使用中、マーカー読み取り問題に直面しました。
分析中に発見したことは、MetaEditorでドキュメントをフォーマットする際、スペースまたは表作成(状況による)がコメント行に追加されるということがよく起こります。マーカー決定時に特定のシンボルの前後にあるスペースを削除することで問題は解決しました。この機能はスクリプトで自動として認識されますが、そこにはリマークがあります。
マーカー名はスペースで始まったり終わったりしません。
終了マーカーは必ずしも必要ではありません。それがなければコードはファイルの最後まで処理されます。ただし、開始マーカーは必ず必要です。マーカー名は定数なので、変数の代わりに#define プロセッサ命令を使います。
#define startread "//start point" #define endread "//end point"
配列タイプを形成するために、関数ボイド ParserInputType(int i,string &type_d[],string text)を作成しました。これは type変数を用いた値をtype_dates[] 配列に書き込むのです。
スクリプトがファイル名およびマーカー名を受け取ると、ファイルを読み始めます。ドキュメントのフォーマットを保存するために、配列で見つけた行を保存しながらスクリプトは情報行を一行ずつよにます。
すべてを一つの変数としてフラッシュすることも可能ですが、今回の場合は、ハイフンを失い、テキストは無限行となってしまいます。ファイル読み取り関数が新規ストリングを取得する反復における細部を変更するストリング配列を使用するのはそのためです。
//+------------------------------------------------------------------+ //| downloading file | //+------------------------------------------------------------------+ void ReadFile() { string subfolder="Templates"; int han=FileOpen(subfolder+"\\"+folder+".mqh",FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI,"\r"); if(han!=INVALID_HANDLE) { string temp=""; //--- scrolling file to the starting point do {temp=FileReadString(han);StringTrimLeft(temp);StringTrimRight(temp);} while(startread!=temp); string text=""; int size; //--- reading the file to the array until a break point or the end of the file while(!FileIsEnding(han)) { temp=text=FileReadString(han); // deleting symbols of tabulation to check the end StringTrimLeft(temp);StringTrimRight(temp); if(endread==temp)break; // flushing data to the array if(text!="") { size=ArraySize(fdates); ArrayResize(fdates,size+1); fdates[size]=text; } } FileClose(han); } else { Print("File open failed"+subfolder+"\\"+folder+".mqh, error",GetLastError()); flagnew=true; } }
使い勝手がよいようにファイルは FILE_SHARE_READ モードで開きます。それにより編集済みファイルを閉じなくてもスクリプトを開始することができます。ファイル拡張は 'mqh'として指定されます。よって、スクリプトはインクルードファイルに格納されたコードのテキストを直接読みます。問題は、実は 'mqh' 拡張子を伴うファイルがテキストファイルであるということです。ファイルの名前を 'txt' に着けなおし、テキストエディタを使用して 'mqh' ファイルを開けば確認できます。
読み込みの最後では、配列長は開始および終了マーカー間の行数に等しくなります。
ここからは情報分析に入ります。情報分析および置換を行う関数は、ファイル void WriteFile(int count)に書き込む関数から呼ばれます。コメントは関数内部に入れます。
void WriteFile(int count) { ... if(han!=INVALID_HANDLE) { if(flagnew)// if the file cannot be read { ... } else {// if the file exists ArrayResize(tempfdates,count); int count_type=ArraySize(type_dates); //--- the cycle rewrites the contents of the file for each type of the type_dates template for(int j=0;j<count_type;j++) { for(int i=0;i<count;i++) // copy data into the temporary array tempfdates[i]=fdates[i]; for(int i=0;i<count;i++) // replace templates with types Replace(tempfdates,i,j); for(int i=0;i<count;i++) FileWrite(han,tempfdates[i]); // flushing array in the file } } ... }
データがその場所で置き換えられ、変換後配列は変更されるので、コピーに連携します。一時的データ格納に使用される配列 tempfdates[] のサイズを設定し、fdates[] 例に従って書き込みをします。
関数 Replace() を使用するテンプレート代用を処理します。関数のパラメータは以下です。処理される配列(ここでテンプレート代用が行われます。)表カウンター i(配列内部で移動します)、タイプカウンターj (タイプ配列内をナビゲートします。)
ここではネストサイクルが2つあるため、ソースコードは指定タイプの数だけコードが印刷されます。
//+------------------------------------------------------------------+ //| replacing templates with types | //+------------------------------------------------------------------+ void Replace(string &temp_m[],int i,int j) { if(i>=ArraySize(temp_m))return; if(j<ArraySize(type_dates)) StringReplac(temp_m[i],TEMPLAT,type_dates[j]);// replacing templat with types }
関数Replace() にはチェック機能(配列の存在しないインデックスを呼ぶのを避けるため)があり、それはネスト関数 StringReplac()を呼びます。関数名が基本関数StringReplaceの名前に似ているのには理由があります。それらもまた同数のパラメータ数を持つのです。
よって、一文字"e"を追加することで、置き換えのロジック全体を変更することが可能です。 The 標準関数 は 'find' 例の値を取り、それを指定のストリング「置換」と置き換えます。私の関数は置き換えるだけでなく 'find' の前にシンボルがあれば( 'find' が言葉の一部であるかチェックします。)分析もします。もしあれば 'find' を 'replacement' と置き換えますが、前記の場合ではいずれにしてもそのように置き換わります。タイプ設定以外にもオーバーライドされたデータ名として使用することができます。
改善
使用中に追加された改善点についてお話します。スクリプト使用中にマーカー読み出し問題があることはすでにお話しました。
問題は関数 void ReadFile() 内部に次のコードを書くことで解決されます。
string temp=""; //--- scrolling the file to the start point do {temp=FileReadString(han);StringTrimLeft(temp);StringTrimRight(temp);} while(startread!=temp);
サイクル自体は前バージョンに実装されていますが、関数 StringTrimLeft() および StringTrimRight() を使用する表作シンボルの成切り捨ては強化版のみにあります。
また、アウトプットファイルから "templat" 拡張子を外し、アウトプットファイルを使える状態にしたことも改善点の一つです。それは指定ストリングから指定された例の関数を削除することで実装されます。
削除関数のコード
//+------------------------------------------------------------------+ //| Deleting the 'find' template from the 'text' string | //+------------------------------------------------------------------+ string StringDel(string text,const string find) { string str=text; StringReplace(str,find,""); return(str); }
ファイル名を切るcuttingコードは関数 void WriteFile(int count)にあります。
string newfolder; if(flagnew)newfolder=folder;// if it is the first start, create an empty file of pre-template else newfolder=StringDel(folder," templat");// or create the output file according to the template
また、プレテンプレートの準備を行うモードも紹介されています。要求されるファイルがディレクトリFiles/Templates になければ、プレテンプレートファイルとして作成されます。
例
//#define _XXX_ long //this is the start point _XXX_ //this is the end point
この行を作成するコードは関数 void WriteFile(int count) 内にあります。
if(flagnew)// if the file couldn't be read {// fill the template file with the pre-template FileWrite(han,"#define "+TEMPLAT+" "+type_dates[0]); FileWrite(han," "); FileWrite(han,startread); FileWrite(han," "+TEMPLAT); FileWrite(han,endread); Print("Creating pre-template "+subfolder+"\\"+folder+".mqh"); }
コード実行はグローバル変数 flagnewによって保護されています。これはファイル読み取りにエラーがあれば 'true' を返します。
スクリプト使用中、テンプレートを一つ追加しました。第二テンプレートへの連携処理も同じです。変更を要求する関数は、追加テンプレートと連携するため、関数OnStart() の近くに配置します。新規テンプレートに連携するパスはbeatenです。というわけで、好きなだけテンプレートを連携することができます。では処理を見ていきます。
操作確認
まず、要求されるパラメータを指定することからスクリプトを始めます。表示されるウィンドウで、ファイル名「テンプレート例」を指定します。
セパレータ ';' を使って、データのカスタムタイプフィールドに書き込みます。
"OK" ボタンが押されるとすぐ、テンプレートディレクトリが作成されます。そこにはプリテンプレートファイル "Example templat.mqh"が含まれます。
このイベントはメッセージ付きでジャーナルに表示されます。
プレテンプレートを変更し、スクリプトを再び開始します。今回、ファイルはテンプレートディレクトリにすでに存在します(ディレクトリ自体とともに)。よって、ファイルオープンのエラーメッセージは表示されまん。指定のテンプレートに従い置換が行われます。
//this_is_the_start_point _XXX_ Value_XXX_; //this_is_the_end_point
作成されたファイル "Example.mqh" を再び開きます。
long ValueLONG; double ValueDOUBLE; datetime ValueDATETIME; string ValueSTRING;
ご覧のとおり、パラメータとして渡したタイプ数に応じ、1行から4行が作成されます。次にテンプレートファイルに以下の2行を書きます。
//this_is_the_start_point _XXX_ Value_XXX_; _XXX_ Type_XXX_; //this_is_the_end_point
結果として、明確にスクリプト操作ロジックが表示されます。
まず、コード全体は一つのデータタイプで書かれ、それから別タイプの処理が行われます。これはすべてのデータタイプが処理されるまで行われます。
long ValueLONG; long TypeLONG; double ValueDOUBLE; double TypeDOUBLE; datetime ValueDATETIME; datetime TypeDATETIME; string ValueSTRING; string TypeSTRING;
そして例のテキスト内に二番目のテンプレートをインクルードします。
//this_is_the_start_point _XXX_ Value_XXX_(_xxx_ ind){return((_XXX_)ind);}; _XXX_ Type_XXX_(_xxx_ ind){return((_XXX_)ind);}; //this_is_the_end_button
結果
long ValueLONG(int ind){return((long)ind);}; long TypeLONG(int ind){return((long)ind);}; double ValueDOUBLE(float ind){return((double)ind);}; double TypeDOUBLE(float ind){return((double)ind);}; datetime ValueDATETIME(int ind){return((datetime)ind);}; datetime TypeDATETIME(int ind){return((datetime)ind);}; string ValueSTRING(string ind){return((string)ind);}; string TypeSTRING(string ind){return((string)ind);};
最後の例では、わざと最終行のあとにスペースを入れました。そのスペースはスクリプトが一タイプ処理を終え、別のタイプ処理を始める箇所を表しています。二番目のテンプレートに関して、タイプ処理は最初のテンプレート同様に行われることに留意します。同じタイプが最初のテンプレートになければ、なにも印刷されません。
では、コードデバッグの疑問を解決したいと思います。挙げられているのはデバッグの極簡単な例です。プログラミング中、コードのかなりおおきな部分をデバッグし、それがおわるとすぐに掛け合わせる必要があるかもしれません。それにはプリテンプレートにコメント行が保持されています。 "//#define _XXX_ long"です。
このコメントを移動させれば、テンプレートは実タイプとなります。要するに、コンパイラに対してテンプレートをどのように解釈するか伝えるのです。
しかし残念ながら、この方法ですべてをデバッグするわけにはいきません。ただ、一タイプはデバッグし、テンプレートタイプを 'define'; に変更することができます。ということは全タイプを一つずつデバッグすることは可能です。もちろん、デバッグには呼ばれたファイルのディレクトリまたはインクルードディレクトリにファイルを移動する必要があります。これは以前、疑似テンプレートのデメリットをお話したとき申し上げたデバッグの欠点です。
おわりに
結論として申し上げたいのは、疑似テンプレート使用は考え方として面白く、生産性のあるものですが、実装の小さな開始についての考えにしかすぎません。前述のコードは動作しているし、コードを書く上でかなりの時間を節約しましたが、また多くの疑問は未解決のままです。最初に、標準を開発する疑問です。
私のスクリプトはテンプレート置換ブロックを実装しています。しかし、この手法は必ずしも必要なものではありません。特定ルールを解釈するもっと複雑な分析手法も作成することができます。ここでお話したことはまたまだはじまりです。もっと大きな議論を期待しています。指向はコンフリクトを呼びます。ご健闘をお祈りします!