
初級から中級まで:テンプレートとtypename(II)
はじめに
前回の「初級から中級まで:テンプレートとtypename (I)」では、関数やプロシージャのテンプレート作成という、非常に複雑ですが非常に魅力的なテーマについて話し始めました。このテーマは一度に考えたり説明したりするには難しいため、いくつかの記事に分けて取り扱います。ただし、このテーマだけを深掘りし続けるわけではありません。他にも同様に興味深いテーマがあり、それらに触れることで初めて理解できることも多いからです。
そのため、ある程度しっかりと幅広い基盤を作った段階で、別のテーマへ進み、また改めてテンプレートに戻ってきます。というのも、このテーマは非常に広範囲に及ぶからです。いずれにしても、前回の記事であえて触れなかったいくつかのポイントをここで明確にしておく必要があります。問題を複雑にしすぎないために、前回はそれらを省略しました。この記事を通じて、あなたが快適に学びを進められ、少なくともMQL5で利用可能な各ツールについて良い理解を持ち、より正しく安全にプログラミングを始められるようになることを目指しています。
ここで述べる内容の多くは他の言語にも当てはめることができますが、それぞれの概念の適用にあたっては十分な注意が必要です。
では、少しリラックスして、気が散る要素を取り除き、これから解説する内容に集中してください。この記事では、テンプレートについてさらに詳しく見ていきます。
ここで提供される教材はあくまでも教育的な目的のためのものです。提示された概念の学習以外の最終的な用途として利用すべきものではありません。
テンプレート、さらにテンプレート
テンプレートについて最も興味深い点のひとつは、適切に計画すれば欠かせないツールになるということです。これは、いわゆる「迅速な実装モデル」を作り出すことになるからです。つまり、すべてを一からプログラムする必要はなく、必要な要素の一部だけを作成すればよいのです。
おそらく、今は何のことか分からないかもしれません。ですが、プログラミングを学び実践していくうちに、ある手順を踏めば多くのことをより速くおこなえることに気づくでしょう。自分たちが使えるすべてのツールを理解することで、最適な道を選ぶことができます。名前にとらわれてはいけません。受け入れられている概念を理解することに努めれば、やがて自分で舵を取り、自分自身の道を築いていけるでしょう。
では、前回の記事で示した内容に基づき、とてもシンプルな実装から始めてみましょう。以下に示します。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(Sum(10, 25)); 07. Print(Sum(-10.0, 25.0)); 08. } 09. //+------------------------------------------------------------------+ 10. template <typename T> 11. T Sum(T arg1, T arg2) 12. { 13. Print(__FUNCTION__, "::", __LINE__); 14. return arg1 + arg2; 15. } 16. //+------------------------------------------------------------------+
コード01
前回の記事で扱ったコード01では、異なる型のデータを処理するために同じ関数を利用できる機会がありました。ところが、控えめに言っても少々厄介な点があります。問題は、6行目と7行目で異なる型のデータを扱っていることです。前回の記事の内容を学習したあとならお気づきかもしれませんが、コンパイラはこの問題をオーバーロード関数を生成することでうまく処理します。つまり最終的には、どのデータ型が使われているのかを自分で判別する必要はありません。しかしここに不都合な点があります。それは、ここで使うデータ型は同じでなければならないということです。つまり、Sum関数テンプレートに渡す最初の引数がfloat型である場合、2番目の引数も必ずfloat型である必要があります。そうでない場合、コンパイラはエラーを出すか、よくても「使用されているデータ型に問題がある」という警告を表示します。
より理解を深めるために、行をひとつ変更してみましょう。これはコード01の6行目または7行目のどちらでも構いません。そうすれば、異なる型が関数に渡されることになります。ここで覚えておいてほしいのは、その関数はまだ実際には存在していないということです。コンパイラは、与えられたテンプレートに基づいて関数を構築しなければならないのです。したがって、コード01は次のように変更されます。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(Sum(10, 25)); 07. Print(Sum(5, 35.2)); 08. } 09. //+------------------------------------------------------------------+ 10. template <typename T> T Sum(T arg1, T arg2) 11. { 12. Print(__FUNCTION__, "::", __LINE__); 13. return arg1 + arg2; 14. } 15. //+------------------------------------------------------------------+
コード02
関数に渡された値だけが変更されていることに注意してください。今回の場合、コード01とコード02を比較するとわかるように、7行目を変更することにしました。しかし、このコードのわずかな、しかも一見無害な変更にもかかわらず、コンパイルを試みたときに何が起こったかをご覧ください。
図01
図01に示されたメッセージデータは、具体的なケースによって多少異なる場合があります。読者は混乱するかもしれません。なぜなら、7行目の値を変えただけなのに、コンパイラが基準となる参照を確立できなかったからです。これこそ、先ほど述べた「厄介な点」です。今回の例は純粋に教育的なものなので、読者の中には「なぜコンパイラは与えられたテンプレートに基づいてコードを生成できないのか?」と考える人もいるでしょう。理由はこうです。最初の引数が整数型の場合、2番目の引数に浮動小数点型の値を入れることはできません。その逆も同様で、最初に浮動小数点型を入れて、その次に整数型を入れることもできません。
コード02でSum関数テンプレートが定義されているので、コンパイラはテンプレート関数、すなわちSum関数から期待される処理をおこなうのに適した関数を生成できないのです。この問題に直面すると、多くの初心者は諦めて別のアプローチを選びがちです。しかし実際には、とてもシンプルに解決できるのです。ただし、この問題を正しく解決するには、まずテンプレートとして使われる関数や手続きが何を期待されているのかを理解し、それをもとに他のオーバーロードされた手続きや関数を作成する必要があります。
今回の目的はあくまで教育的なものなので、使用する関数はとても単純です。期待しているのは「2つの値を足して、その和を返すこと」だけです。もし浮動小数点型の値に整数型の値を加えた経験があれば、結果は浮動小数点型になることを覚えているはずです。
このようなことは「型変換」あるいは「キャスト」と呼ばれます。変数や定数について説明した際にも触れました。基本的に、次のような結果になります。
図02
図02に基づくと、異なる型で数値演算がおこなわれる場合には、常にdouble型が使用されることがわかります。これはコード02の7行目でも同じです。このこと、そしてコンパイラがオーバーロード関数ではなくテンプレートを使うことを理解すれば、コンパイラに「こちらが状況を理解している」ということを示すことができます。したがって、コード02を次のように変更すれば、実際に動作するようになります。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(Sum(10, 25)); 07. Print(Sum((double) 5, 35.2)); 08. } 09. //+------------------------------------------------------------------+ 10. template <typename T> T Sum(T arg1, T arg2) 11. { 12. Print(__FUNCTION__, "::", __LINE__); 13. return arg1 + arg2; 14. } 15. //+------------------------------------------------------------------+
コード03
コード03では、コード02と同じコードを使用しています。ただしここでは、値が整数型の変数であっても double型を扱っていることを、コンパイラに明示的に理解させています。この明示的な型変換は、コード03の7行目に一語を追加することでおこなわれます。これにより、コード02の7行目とコード03の7行目とを区別でき、コンパイラはSumテンプレート関数を利用して適切なオーバーロードを生成できるようになります。その結果、最終的には次のようになります。
図03
実際のところ、ここで示した解決方法は唯一のものではありません。少しやり方を変えても、同じ結果を得ることができます。大切なのは、何を実装したいのか、そして今の目標が何なのかを正確に理解することです。したがって、単にコードを暗記するだけでなく(あるいはもっと悪いのは、CTRL+CとCTRL+Vでコピー&ペーストすること)、使用されている概念を本当に理解する必要があります。これが唯一、発生するあらゆるプログラミング関連の問題を解決できる方法なのです。
もうひとつの解決策は、引数のひとつを特定の型に制限することです。多くの人はこの方法を適切だとは考えませんが、特定の状況において、ある引数に常に特定の型の情報を使うと事前にわかっている場合には、さまざまな問題を解決することができます。
したがって、最初の引数が常に整数型であると仮定すると、次のようにすることができます。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(Sum(10, 25)); 07. Print(Sum(5, 35.2)); 08. } 09. //+------------------------------------------------------------------+ 10. template <typename T> T Sum(short arg1, T arg2) 11. { 12. Print(__FUNCTION__, "::", __LINE__); 13. return arg1 + arg2; 14. } 15. //+------------------------------------------------------------------+
コード04
このコードの結果はコード03、すなわち画像03と同じです。ただしコード04では、テンプレート関数の引数のひとつが常に整数型でなければならないことをコンパイラに伝えています。この場合、符号付き16ビット型を使いましたが、他の型でも構いません。この状況では明示的な型変換をおこなう必要はありません。なぜなら、この状況でコンパイラがどうすべきかをすでに理解しているからです。ただし重要なのは、2つ目の引数は実行ファイルを生成する段階でコンパイラによって決定されるという点です。したがって、6行目(整数型だけを使う場合)に応答する関数と、7行目(整数型と浮動小数点を使う場合)に応答する関数の2つが生成されます。
さて、ここでお聞きします。こうしたシナリオで遊びながら練習するのは楽しくありませんか。よく考えてみてください。私たちはコンパイラに、どのように動作すべきかを伝えているのです。そしてその結果、同じことを開発するためにさまざまな可能性を簡単に作り出せているのです。
しかし、これで終わりだとは思わないでください。この問題を解決する別の方法も存在します。ただし、この場合は少し複雑になり、別の種類の問題が発生します。その解決策についてはまた別の機会に示すことにしましょう。とはいえ、この方法はさまざまな状況に適しており、MQL5の利用や活用において新たな可能性への扉を開くものでもあります。
ただし混乱を避けるため、これについては別のトピックで取り上げます。まずはこれらの概念を学び、それから次に進むようにしてください。
1つのテンプレート、複数の型
前のトピックでは、テンプレートを使う際に私たちを制限することもある、やや不愉快な問題への対処方法を説明しました。しかし、そこで学んだことは実際にできることの第一歩にすぎません。これから説明する内容は、少なくともMQL5においてはあまり一般的ではありません。私の記憶では、同じ手法を使っている人を見たことはありません。多くの人は、そのような方法は存在しない、あるいは実現できないと考えるかもしれません。そうなると、自由度が失われ、主な目的を達成できなくなってしまいます。
しかし「見たことがない」「聞いたことがない」からといって、それが存在しないとか、プログラミング言語で認められていないという意味にはなりません。多くの場合、問題の原因はそこにあるのではなく、むしろ誤用や(ほとんどのケースでは)プログラミングツールに関連する概念の誤解にあります。
前回の記事で説明したように、テンプレートで使われるTは、実際にはコンパイラが特定の型を局所的に識別するために使う識別子です。これは、入力される情報をどのように扱うべきかを知るために必要なのです。識別子の概念を理解していれば、それが正しく宣言されている場合、変数や定数のように動作することが分かるでしょう。
ただし、コード04の10行目にあるTのような識別子は変数ではありません。なぜなら、識別後に変更することはできないからです。では定数なのか。その通りですが、それはコンパイラによって定義される、期待されるデータ型を表す定数なのです。
注意してください。ここでデータ型と言うとき、私たちは画像02で示された内容を指しています。したがって、公式や実装モデルを暗記するのではなく、概念そのものを理解することが重要です。この概念は単純ですが、(すぐに気づくように)非常に強力です。この概念を理解すれば、テンプレートが関数や手続きを生成する方法を理解した「ほぼ魔法のような解決策」を生み出すことができます。そのためには、許されるケースをできるだけ多くカバーできるように、必要なだけ識別子を追加すればよいのです。この判断は、コードを実装する段階であなたがおこなうことになります。
そこで、コード04を変更し、コード02により近いものにすることができます。ただし、コード02で見つかった「ひとつのデータ型しか使えない」という問題は回避できます。複雑に聞こえるかもしれませんが、実際には想像するよりもはるかにシンプルです。ここでは、複数の型識別子を使う新しい概念を応用した問題解決の例を見てみましょう。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print(#X, " => ", X, "\n"); 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. PrintX(Sum(10, 25)); 09. PrintX(Sum(5, 35.2)); 10. } 11. //+------------------------------------------------------------------+ 12. template <typename T1, typename T2> T1 Sum(T1 arg1, T2 arg2) 13. { 14. Print(__FUNCTION__, "::", __LINE__); 15. 16. return (T1)(arg1 + arg2); 17. } 18. //+------------------------------------------------------------------+
コード05
ここからが本当に複雑な部分になります。段階を追って慎重に説明してきましたが、それでも問題は存在します。その問題が生じるのは、まさに12行目でSum関数のテンプレートを宣言している部分です。
コード05を実行すると、次の画像のような結果になります。
図04
この画像では、読者の注意を引くためにある一点を強調しています。ここで注目すべきなのは、演算の結果が誤っているということです。正確に言えば、期待される値と一致していないのです。なぜなら、期待される値は画像03で得られたものと同じはずだからです。でも、なぜでしょうか。考えられるのは、12行目の書き方に原因があるのではないかということです。確かにその書き方は意味をなしていないように見えます。しかし、実際の問題は12行目ではなく、コードの解析方法や本来どう実装すべきだったかによって、9行目または16行目にあるのです。したがって、受け入れられた概念を理解し、場当たり的にコードを書く前に必ず考えることが重要です。
私の言葉をそのまま信じる必要はありません。そこで今回は16行目には触れず、9行目を修正して値の宣言順序を入れ替えてみます。これによって、次のようなコードになります。これにより、以下のコードが実現します。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print(#X, " => ", X, "\n"); 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. PrintX(Sum(10, 25)); 09. PrintX(Sum(35.2, 5)); 10. } 11. //+------------------------------------------------------------------+ 12. template <typename T1, typename T2> T1 Sum(T1 arg1, T2 arg2) 13. { 14. Print(__FUNCTION__, "::", __LINE__); 15. 16. return (T1)(arg1 + arg2); 17. } 18. //+------------------------------------------------------------------+
コード06
変更したのは記述した部分のみであり、コード実行の結果は以下に示すとおりです。
図05
「なんて馬鹿げた、意味のないことなんだ。これでは、もっと理解できるようになるまでプログラミングに取り組むのが怖くなってしまう。すべてがとても簡単に思えたのに。動く小さなコード断片を書けたので、自分をプログラマーだとさえ思っていた。しかし、今こうして見ると、自分は何も知らないのだと気づいた。ようやく、本当のプログラマーになるとはどういうことかを学び始めたところなのだ。」
落ち着いてください。実際のところ、それほど悪い話ではありません。確かに、物事は最初に見えるほど単純ではないことが多いのです。特に、自分の「快適ゾーン」にとどまり続けてしまうとそうなります。問題は、多くの人が自分を「優れたプログラマー」だと思い込んでしまい、そのせいで学ぶことをやめてしまう点にあります。だからこそ私は断言します。
優れたプログラマーは決して学ぶことをやめません。。常に知識を更新し、新しい概念を探求し続けます。常にです。
今回のようなことは、プログラマーの士気を打ち砕くものです。特に、一見正しいコードを扱っているにもかかわらず、正しくない結果を返してしまう場合にそうなります。そのコードは本質的に正しいのです。しかし、なぜか常に間違った結果を返す。だからこそ、自分をだましてはいけません。コードを書けるだけでは、プログラマーとは言えないのです。そのレベルに達するには、数多くの経験を積まなければなりません。そして多くの場合、それを教えてくれるのは時間だけです。私自身も同じことを経験しました。コードがあるときはうまく動くのに、別のときにはおかしくなり、一見無意味な結果を返す。そんな理由を理解できるようになるまで、長い時間がかかりました。
では、コード05とコード06で何が起きているのかを見ていきましょう。両者は同じコードであり、唯一の違いは9行目でパラメータを宣言する順序を変えただけです。
コンパイラは12行目で宣言されているテンプレート呼び出しに出会うと、各引数で使用されているデータ型を確認します。そして、これまでと同じように、その呼び出し専用のオーバーロード関数を生成します(既存の関数では対応できない場合)。
12行目に2つのtypenameがあるため、2種類の異なるデータ型を指定できます。これにより、以前は不可能だった、まったく異なるデータ型を使うことが可能になります。整数型でも浮動小数点型でも自由に扱えるので、多くのケースを完全にカバーできます。ただし、特定の問題のため、すべてのケースをカバーできるわけではありません。とはいえ、整数と浮動小数点の両方を同時に、しかも問題なく使えるということを知れば、ほぼ完璧なテンプレートを作れるのです。
したがって、コンパイラは最初の引数のデータ型を定数T1に格納し、2つ目の引数のデータ型を定数T2に格納します。そして、16行目で型変換をおこなってSum関数の戻り値型に一致させれば、画像04や画像05に示されたような結果を得ることができます。
ただし、テンプレートコードを見ただけでは、コンパイラが実際に組み立てるものを漠然としか想像できないため、9行目の値の宣言順序を変えただけでこれほど異なる結果になる理由を理解するのは難しいのです。
より理解を深めるために、コンパイラが両方の場合に書き出す関数を見てみましょう。テンプレートを使わず、伝統的な方法でコードを書いたと仮定すると、コード05は次のようになります。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print(#X, " => ", X, "\n"); 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. PrintX(Sum(10, 25)); 09. PrintX(Sum(5, 35.2)); 10. } 11. //+------------------------------------------------------------------+ 12. int Sum(int arg1, int arg2) 13. { 14. Print(__FUNCTION__, "::", __LINE__); 15. 16. return (int)(arg1 + arg2); 17. } 18. //+------------------------------------------------------------------+ 19. int Sum(int arg1, double arg2) 20. { 21. Print(__FUNCTION__, "::", __LINE__); 22. 23. return (int)(arg1 + arg2); 24. } 25. //+------------------------------------------------------------------+
コード07
コード07を実行すると、まさに図04に示された結果が得られます。そしてコード06を、もし従来のプログラミングモデルで作成していたなら、その内部内容は以下のようになっていたはずです。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print(#X, " => ", X, "\n"); 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. PrintX(Sum(10, 25)); 09. PrintX(Sum(35.2, 5)); 10. } 11. //+------------------------------------------------------------------+ 12. int Sum(int arg1, int arg2) 13. { 14. Print(__FUNCTION__, "::", __LINE__); 15. 16. return (int)(arg1 + arg2); 17. } 18. //+------------------------------------------------------------------+ 19. double Sum(double arg1, int arg2) 20. { 21. Print(__FUNCTION__, "::", __LINE__); 22. 23. return (double)(arg1 + arg2); 24. } 25. //+------------------------------------------------------------------+
コード08
コード07が図04の結果を出すように、コード08は図05の結果を出します。ここで注意してほしいのは、両者の違いが非常に微妙で、多くの状況では気づかれずに見過ごされてしまうということです。つまり、問題が発生しても気づかない可能性があるのです。しかし、テンプレートを使わないコード07やコード08であれば、問題の原因はすぐに特定できます。結果を見れば、明示的な型変換がおこなわれていることが分かります。これが原因でコードが誤った応答を返し、結果的に「問題を修正している」ように見えているのです。
ところが、テンプレートを使用している場合はそう簡単には気づけません。この場合、問題を理解するのは難しく、おそらく途中でやめてしまおうと思うかもしれません。強い意志を持って続けていれば、最終的に間違いの原因を突き止められるでしょう。しかし、それならコード05やコード06の16行目でdouble型への明示的キャストを使えばいい。それなら端末での応答の問題は確実に解決するはずだと考えるかもしれません。
ですが、実際にはこれで問題は解決しません。場合によっては新しい問題を生み出すことさえあります。その理由は単純です。あなたはコード9行目の結果を修正しようとしますが、その前にある8行目を忘れてしまうからです。そこでは整数型のデータを使っています。そのため、問題を解決するどころか、かえって状況を混乱させ、別の場所に新たな問題を生み出してしまう可能性があります。
これは典型的な状況であり、とても厄介なものです。そのため、少なくともMQL5では、複数の型を扱うテンプレートを実際のコードで使う例はほとんど見られません。C言語(特にC++)ではこうしたことは非常に一般的で、日常的に起こります。
したがって、ここで示した内容は日常的に役立つというよりも「興味深い」ものだといえます。しかし、必要に迫られたときには、どのような潜在的問題を引き起こすのかをすぐに思い出せるでしょう。そして、そのような衝突を解決するための独自の方法を考えてみてください。私の知る限り、この問題を簡単に解決する方法はありません。C++ですら、こうした問題に直面すると、自力で回避策を見つけるしかないのです。そして正直に言えば、それはまったく楽しいことではありません。
最終的な考察
この記事では、プログラミングにおいて最も厄介で難しい状況のひとつである、同じ関数または手続きのテンプレートで異なる型を使うことについて説明しました。これまでは関数に焦点を当てていましたが、ここで扱った内容は手続きにもそのまま応用できます。値渡しでも参照渡しでも同様です。ただし、冗長にならないよう、ここでは具体的な例は示しませんでした。
したがって、ここで合意したいのは、ぜひ練習してみてくださいということです。小さなコード片を自分で書き、今回触れたケース、つまり逐次リンクを使った処理や手続きテンプレートの使用などを試してください。現段階ではそれらを必ずしも本格的に使うわけではありませんが、学習の助けになるでしょう。
今回紹介した多くのコードはアプリケーションに含まれています。含まれていないコードも、アプリケーションにあるコードの単純な修正版にすぎません。いずれにしても、ここで示した内容を練習することで理解が深まり、習得につながります。次回の記事でもテンプレートについてさらに詳しく取り上げますので、またすぐにお会いしましょう。
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/15668
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索