
リプレイシステムの開発(第64回):サービスの再生(V)
はじめに
前の記事「リプレイシステムの開発(第63回):サービスの再生(IV)」では、チャート上でバーを構築する際に使用されるティックの最大数をユーザーが調整できる仕組みを開発・実装しました。このコントロールの主な目的は、バーの構築処理がリプレイ/シミュレーターアプリケーションの他の重要な処理に干渉しないようにすることです。ただし、この調整によって表示される実際の出来高に影響を与えたり、歪めたりすることはありません。
しかし、その記事に付属する動画を注意深く見た方や、実際にアプリケーションをコンパイルしてテストされた方は、システムが時折予期せず一時停止モードに入ることに気づかれたかもしれません。奇妙なことに、コントロールインジケーターには何の変化も見られません。このように、なぜ再生モードから突然一時停止モードに切り替わったのかは明確ではありませんでした。正直に言うと、これは本当に奇妙な現象だと認めざるを得ません。皆さんも「どうしてそんなことが起きるのだろう?」と感じたかもしれません。実際、私もまったく同じ疑問を抱き、なぜシステムが一時停止するのか、しかも毎回特定のポイントで発生するのかを突き止めようとしました。次のセクションでは、私がどのようにこの問題を理解し、どのような解決策を考え出したのかについて説明します。
自動一時停止モードの理解と解決
本当の問題は、リプレイ/シミュレーターが自動的に一時停止モードへ切り替わる理由を単に理解することではありません。真に厄介なのは、MetaTrader 5の開発者によって近いうちに対処される可能性が高いとはいえ、執筆時点では、グラフィカルオブジェクトを使った一部のテストシナリオやユースケースが、非常に厄介な問題を引き起こしているという事実に気づいたことです。なぜなら、特定のオブジェクトの状態が、明確な理由や説明もなく変化する可能性があるからです。
少し厳しく言いすぎているかもしれません。しかし、こうしたオブジェクトがリプレイ/シミュレーターアプリケーションにとっていかに重要であるか、そして私たちがそれらをどのように使って制御を維持しているのかを思い出してみましょう。これは特に、このリプレイ/シミュレーターに関する連載をこれまで読んでいない方にとって、重要なポイントです。リプレイ/シミュレーターサービスが初期化されると、まずチャートが開かれ、ティックがファイルから読み込まれ、カスタム銘柄が作成され、そして最後にチャートにインジケーターが配置されます。このインジケーターが、リプレイ/シミュレーターの動作を管理する「コントロールインジケーター」です。
この段階では、コントロールインジケーターとサービスの間で情報をやり取りするために、ターミナルグローバル変数はもはや使用されていません。その代わりに、ユーザーによるデータ干渉を防ぐことができる、新しい情報フローの方法を採用しました。
基本的に、サービスはカスタムイベントを使用してコントロールインジケーターに情報を送信します。そしてコントロールインジケーターは、その情報の一部をバッファを通じてサービスに返します。「一部」と言うのは、バッファを常に読み取り続けることを避けるために、ある特定の情報は異なる方法で送信しているからです。バッファからの読み取りは、必要以上のデータ転送を伴う可能性があります。このため、サービス側では、コントロールインジケーターが管理しているあるオブジェクトにアクセスすることで、その情報を取得します。ただし、オブジェクト自体を変更することはありません。そのオブジェクトとは、現在の実行状態を示すボタン、つまりシステムが再生モードなのか一時停止モードなのかをユーザーが指定するためのボタンです。
この動作は、C_Replay.mqhファイルのソースコードを見ることで確認できます。より具体的に示すため、以下に、C_Replay.mqhクラスの一部となるコードスニペットを示します。35. //+------------------------------------------------------------------+ 36. inline void UpdateIndicatorControl(void) 37. { 38. static bool bTest = false; 39. double Buff[]; 40. 41. if (m_IndControl.Handle == INVALID_HANDLE) return; 42. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 43. { 44. if (bTest) 45. m_IndControl.Mode = (ObjectGetInteger(m_Infos.IdReplay, def_ObjectCtrlName((C_Controls::eObjectControl)C_Controls::ePlay), OBJPROP_STATE) == 1 ? C_Controls::ePause : C_Controls::ePlay); 46. else 47. { 48. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 49. m_IndControl.Memory.dValue = Buff[0]; 50. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) 51. if (bTest = ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)) 52. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 53. } 54. }else 55. { 56. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 57. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 58. m_IndControl.Memory._8b[7] = 'D'; 59. m_IndControl.Memory._8b[6] = 'M'; 60. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 61. bTest = false; 62. } 63. } 64. //+------------------------------------------------------------------+
C_Replay.mqhファイルからのソースコードの元の断片
まず注目すべきは、38行目にあるstatic変数です。これは、オブジェクトを直接読み取るべきか、それともコントロールインジケーターのバッファから読み取るべきかをプロシージャに知らせる役割を担っています。60行目でカスタムイベントを送信すると、61行目で明示的に「次回の呼び出しではインジケーターのバッファから読み取る」ことを強制します。これにより、常にバッファを読み取るのではなく、一定の間隔を空けて時々のみアクセスすることで、リプレイ/シミュレーターサービスへのパフォーマンス負荷を最小限に抑えることができます。しかし、真の仕掛けは51行目にあります。ここでは、バッファを使用せず、代わりにグラフィカルオブジェクトへ直接アクセスするようにシステムに指示しています。ただし、これは再生モード時に限られます。一時停止モードでは、応答速度はさほど重要ではないためです。
つまり、再生モード中は、3回目以降の呼び出しから45行目の処理が実行されます。この処理は、ユーザーが明示的にシステムを一時停止モードに切り替えるまで続きます。ユーザーが一時停止を指示すると、C_ReplayクラスのLoopEventOnTime関数は終了します。ただし、ユーザーが指示したのは「一時停止にすること」だけであり、LoopEventOnTime関数は再度呼び出され、再びコントロールインジケーターの監視を開始します。この時点では、制御状態をグラフィカルオブジェクト経由で監視しています。そしてここで、別の方法を取れないという制約に直面します。ですが、これが自動的な一時停止への切り替えの原因ではありません。サービス側が一時停止をトリガーしているのではなく、それはコントロールインジケーターの内部で発生しています。そして、その原因が、先ほどのスニペットの60行目で送信されたカスタムイベントなのです。ここから事態はさらに複雑になります。なぜ、カスタムイベントを送るという行為が、ユーザーが一時停止モードに切り替えたという誤った情報を、インジケーターがサービスに送ることにつながるのでしょうか。それはまったくもって不可解です。正直、かなり奇妙な現象です。ですが、どうやらそのカスタムイベントによって、インジケーターが、サービス側が監視しているオブジェクトの OBJPROP_STATE プロパティを変更してしまうようなのです。このプロパティが変更されると、45行目の処理によって、サービスは「インジケーターが一時停止モードに切り替えた」と誤って判断してしまいます。その結果、LoopEventOnTime関数は終了し、再初期化されることになります。しかし、再度 LoopEventOnTimeが実行されて OBJPROP_STATEの値を確認した際、そこには誤った値が残っているのです。実際にはインジケーターが再生モードを維持しているのに、サービス側は誤って一時停止モードに切り替わってしまいます。
さて、この問題の仕組みを本当に理解したなら、おそらく、「そもそもチャート上のオブジェクトを監視しているのが間違いで、インジケーターのバッファを読むべきだったのでは」と考えるはずです。はい、そのとおりです。私も同意します。根本的な問題は、サービスがOBJPROP_STATEをチャートオブジェクトから読み取っている点にあります。それは本来アクセスすべきではない場所です。これにも同意します。しかし、それでもなお、カスタムイベントをトリガーしただけでOBJPROP_STATEが変更されるという事実が正当化されるわけではありません。一つの過ちが、別の過ちを正当化することにはならないのです。いずれにせよ、この問題には2つの直接的な解決策があります。1つは、チャートオブジェクトの状態を観察するための別のプロパティを使用することです。これで問題は回避できますが、この方法は採用しません。その理由は、別の問題…というよりも、まだ実装していない重要な機能があるからです。
インジケーターバッファから読み取る処理は、チャートオブジェクトを監視するよりも多少時間がかかるかもしれませんが、私はその方法を選びました。というのも、現在のバージョンでは利用できないものの、以前のバージョンには存在していた「早送りモード」という機能を、これから実装する予定だからです。そのため、必要な変更を加え、ルーチンから静的変数を取り除いた結果、最終的な実装は以下のようになります。
35. //+------------------------------------------------------------------+ 36. inline void UpdateIndicatorControl(void) 37. { 38. double Buff[]; 39. 40. if (m_IndControl.Handle == INVALID_HANDLE) return; 41. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 42. { 43. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 44. m_IndControl.Memory.dValue = Buff[0]; 45. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) 46. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 47. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 48. }else 49. { 50. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 51. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 52. m_IndControl.Memory._8b[7] = 'D'; 53. m_IndControl.Memory._8b[6] = 'M'; 54. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 55. } 56. } 57. //+------------------------------------------------------------------+
C_Replay.mqhファイル内の変更されたソースコードの一部
順調とは言えない部分もありましたが、今回の手法は、最終的には私たちにとって最善の解決策であることが分かりました。たとえその過程で、特定のオブジェクトの特定のプロパティは完全には信頼できないという、少し奇妙な事実が明らかになったとしても、です。少なくとも、この記事を書いている現時点ではそう言わざるを得ません。とはいえ、何らかの理由でどうしてもサービス側からチャート上のオブジェクトを読み取りたいという場合には、OBJPROP_TOOLTIPプロパティの使用をお勧めします。これは文字列型のプロパティですが、「再生中」か「一時停止中」かを判定する情報の転送に使った際、私のテスト環境では特に問題は発生しませんでした。ただし、このオブジェクトアクセスによる方法が一見うまくいくように見えても、早送り機能を正しく実装するには適していませんでした。この手法を取る場合、コード全体にわたって多数の追加修正が必要となり、最終的には UpdateIndicadorControlプロシージャ自体を修正し、示された通りの動作をするようにしなければならないのです。
この問題についてはこれで一区切りとし、次は、同様に悩まされた別の課題に移りましょう。この問題は、記事「リプレイシステムの開発(第62回):サービスの再生 (III)」のビデオでも紹介されています。その特定の問題がどのように解決されたかを説明するために、次のトピックへ進んでいきましょう。
メモリダンプ問題の解決
実行中にアプリケーションが突然クラッシュするのを見て、驚いた方も多いかもしれません。特に、それがチャートを閉じようとしたとき、あるいはすでに閉じられていたときに発生した場合です。経験の浅い開発者であれば、この障害の原因をすぐに MetaTrader 5やMQL5そのものに求めがちです。というのも、自分のプログラムが正しく動作していない、あるいは手を加えるべきバグがあると認めるよりも、責任をプラットフォームに押しつけてしまうほうが簡単だからです。知識不足であるか、あるいはバグ修正よりも特定の機能開発を優先しているからかもしれませんが、プラットフォームがすべて面倒を見てくれるだろうと期待するのは、ある意味で「無知の証明」とも言えます。もしくは、開発者としての自分がまだ甘いことを認めているようなものです。実際のところ、プラットフォームは設計された範囲内での動作しかしません。それ以外を適切に処理し、アプリケーション全体を安定させるのは、開発者であるあなたの責任です。
私自身も、正直に言えば、特定の問題の修正やコードの改善を長い間後回しにしてきました。というのも、他の機能の実装に集中しており、そもそもそこまで進めるか分からないと思っていたからです。動作するコード、あるいは必要な機能をとりあえず提供できればそれでよかったのです。バグを修正したあとで仕様に無理があったことがわかり、その修正済みコードを捨てるのは非常に虚しい経験です。そのため、コードにさらに手をかける価値があると確信できたタイミングで、ようやく修正や改善を加えるようにしてきました。
私が物事を複雑にしすぎていると思う人もいるかもしれません。最初から完成度の高いコードをお見せすることもできるでしょう。しかし、それでは初心者に「コードとは最初から完璧なもの」「すべての機能が最初から揃っているもの」という、間違った印象を与えてしまいます。経験豊富な開発者であれば、そんなことはあり得ないと分かっているはずです。コードとは進化していくものであり、問題が発生したときにはどう対処するかが問われるのです。私はその過程も含めて、皆さんにお見せしたいのです。
いずれにせよ、システムの動作を変え始めたあたりから長らく存在していた、ある特定の問題の解決方法を、今こそお見せする時です。コードをきちんと見直してこの問題を理解しようとしなかった場合、おそらく「原因は複数の無関係なバグにある」と考えてしまうでしょう。でもそれは違います。そう思っている方は、本当にこの記事を「学ぶ目的」で読んでいるのではなく、「完成されたコード」を求めているだけかもしれません。しかしそれでは、この記事の目的に反します。この記事の本当の目的は、読者であるあなたに、他のプログラマーが実際に使ったり検証したりしたテクニックや実践方法を紹介し、それを通じて新しい考え方や、異なるアプローチを知ってもらうことにあります。
とはいえ、哲学はもう十分です。このメモリダンプの問題を、実際にどう解決するのかを見ていきます。まず最初に注目すべきは、MetaTrader 5によって報告されるエラーメッセージです。参考までに、以下の画像をご覧ください。
図01:MetaTrader 5によって報告されたエラーの表示
この画像では、特定の2行が強調表示されています。残りの行は、その2行に関連しているものです。注意深く見ると、問題の根本的な原因を特定することができます。このエラーは、特定のオブジェクトがチャートから削除されていないことに起因しています。「そんなことがあり得るのか?」「オブジェクトが削除されていないとはどいうことか?」「削除を忘れたのか?」と思うかもしれません。ですが、その答えは「いいえ」です。オブジェクトの削除はクラスのデストラクタ内で実行されています。これについては、コントロールインジケーターのコードを確認すれば分かります。画像に示されているとおり、問題はまさにその箇所で発生しています。
では、オブジェクトが削除されているのに、なぜMetaTrader 5は「削除されていない」と警告を出すのでしょうか。しかも、削除されていないとされるオブジェクトは、C_DrawImageクラスのインスタンスです。
このような警告を見ると、何をすればいいのか全く分からなくなるかもしれません。特に、C_DrawImage クラスは直接アクセスされるのではなく、間接的に利用されているためです。これをより明確にするために、すべてのオブジェクトの管理を担当するC_Controlsクラスのコードを見てみましょう。このクラスは、C_DrawImageを呼び出す役割も担っています。ここで重要なのは、MetaTrader 5 が警告しているのは、C_DrawImage型のオブジェクトが削除されていないことだということです。つまり、問題はC_DrawImageクラスそのものにあるのではなく、それを使っている側のコード(すなわちC_Controlsクラス)にあることがわかります。
この問題とその解決策を理解するために、すべてのコードを読む必要はありません。以下に、最も重要な部分のみを抜粋して示します。なお、以下に示すコードには、すでにメモリダンプ問題への修正が反映されています。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\Auxiliar\C_DrawImage.mqh" 005. #include "..\Defines.mqh" 006. //+------------------------------------------------------------------+ 007. #define def_PathBMP "Images\\Market Replay\\Control\\" 008. #define def_ButtonPlay def_PathBMP + "Play.bmp" 009. #define def_ButtonPause def_PathBMP + "Pause.bmp" 010. #define def_ButtonLeft def_PathBMP + "Left.bmp" 011. #define def_ButtonLeftBlock def_PathBMP + "Left_Block.bmp" 012. #define def_ButtonRight def_PathBMP + "Right.bmp" 013. #define def_ButtonRightBlock def_PathBMP + "Right_Block.bmp" 014. #define def_ButtonPin def_PathBMP + "Pin.bmp" 015. #resource "\\" + def_ButtonPlay 016. #resource "\\" + def_ButtonPause 017. #resource "\\" + def_ButtonLeft 018. #resource "\\" + def_ButtonLeftBlock 019. #resource "\\" + def_ButtonRight 020. #resource "\\" + def_ButtonRightBlock 021. #resource "\\" + def_ButtonPin 022. //+------------------------------------------------------------------+ 023. #define def_ObjectCtrlName(A) "MarketReplayCTRL_" + (typename(A) == "enum eObjectControl" ? EnumToString((C_Controls::eObjectControl)(A)) : (string)(A)) 024. #define def_PosXObjects 120 025. //+------------------------------------------------------------------+ 026. #define def_SizeButtons 32 027. #define def_ColorFilter 0xFF00FF 028. //+------------------------------------------------------------------+ 029. #include "..\Auxiliar\C_Mouse.mqh" 030. //+------------------------------------------------------------------+ 031. class C_Controls : private C_Terminal 032. { 033. protected: 034. private : 035. //+------------------------------------------------------------------+ 036. enum eMatrixControl {eCtrlPosition, eCtrlStatus}; 037. enum eObjectControl {ePause, ePlay, eLeft, eRight, ePin, eNull, eTriState = (def_MaxPosSlider + 1)}; 038. //+------------------------------------------------------------------+ 039. struct st_00 040. { 041. string szBarSlider, 042. szBarSliderBlock; 043. ushort Minimal; 044. }m_Slider; 045. struct st_01 046. { 047. C_DrawImage *Btn; 048. bool state; 049. short x, y, w, h; 050. }m_Section[eObjectControl::eNull]; 051. C_Mouse *m_MousePtr; 052. //+------------------------------------------------------------------+ ... 071. //+------------------------------------------------------------------+ 072. void SetPlay(bool state) 073. { 074. if (m_Section[ePlay].Btn == NULL) 075. m_Section[ePlay].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(ePlay), def_ColorFilter, "::" + def_ButtonPause, "::" + def_ButtonPlay); 076. m_Section[ePlay].Btn.Paint(m_Section[ePlay].x, m_Section[ePlay].y, m_Section[ePlay].w, m_Section[ePlay].h, 20, (m_Section[ePlay].state = state) ? 1 : 0); 077. if (!state) CreateCtrlSlider(); 078. } 079. //+------------------------------------------------------------------+ 080. void CreateCtrlSlider(void) 081. { 082. if (m_Section[ePin].Btn != NULL) return; 083. CreteBarSlider(77, 436); 084. m_Section[eLeft].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(eLeft), def_ColorFilter, "::" + def_ButtonLeft, "::" + def_ButtonLeftBlock); 085. m_Section[eRight].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(eRight), def_ColorFilter, "::" + def_ButtonRight, "::" + def_ButtonRightBlock); 086. m_Section[ePin].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(ePin), def_ColorFilter, "::" + def_ButtonPin); 087. PositionPinSlider(m_Slider.Minimal); 088. } 089. //+------------------------------------------------------------------+ 090. inline void RemoveCtrlSlider(void) 091. { 092. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 093. for (eObjectControl c0 = ePlay + 1; c0 < eNull; c0++) 094. { 095. delete m_Section[c0].Btn; 096. m_Section[c0].Btn = NULL; 097. } 098. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("B")); 099. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 100. } 101. //+------------------------------------------------------------------+ ... 132. //+------------------------------------------------------------------+ 133. public : 134. //+------------------------------------------------------------------+ 135. C_Controls(const long Arg0, const string szShortName, C_Mouse *MousePtr) 136. :C_Terminal(Arg0), 137. m_MousePtr(MousePtr) 138. { 139. if ((!IndicatorCheckPass(szShortName)) || (CheckPointer(m_MousePtr) == POINTER_INVALID)) SetUserError(C_Terminal::ERR_Unknown); 140. if (_LastError != ERR_SUCCESS) return; 141. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 142. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("")); 143. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 144. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) 145. { 146. m_Section[c0].h = m_Section[c0].w = def_SizeButtons; 147. m_Section[c0].y = 25; 148. m_Section[c0].Btn = NULL; 149. } 150. m_Section[ePlay].x = def_PosXObjects; 151. m_Section[eLeft].x = m_Section[ePlay].x + 47; 152. m_Section[eRight].x = m_Section[ePlay].x + 511; 153. m_Slider.Minimal = eTriState; 154. } 155. //+------------------------------------------------------------------+ 156. ~C_Controls() 157. { 158. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) delete m_Section[c0].Btn; 159. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("")); 160. delete m_MousePtr; 161. } 162. //+------------------------------------------------------------------+ ... 172. //+------------------------------------------------------------------+ 173. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 174. { 175. short x, y; 176. static ushort iPinPosX = 0; 177. static short six = -1, sps; 178. uCast_Double info; 179. 180. switch (id) 181. { 182. case (CHARTEVENT_CUSTOM + evCtrlReplayInit): 183. info.dValue = dparam; 184. if ((info._8b[7] != 'D') || (info._8b[6] != 'M')) break; 185. iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition])); 186. SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay); 187. break; 188. case CHARTEVENT_OBJECT_DELETE: 189. if (StringSubstr(sparam, 0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName("")) 190. { 191. if (sparam == def_ObjectCtrlName(ePlay)) 192. { 193. delete m_Section[ePlay].Btn; 194. m_Section[ePlay].Btn = NULL; 195. SetPlay(m_Section[ePlay].state); 196. }else 197. { 198. RemoveCtrlSlider(); 199. CreateCtrlSlider(); 200. } 201. } 202. break; 203. case CHARTEVENT_MOUSE_MOVE: 204. if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) switch (CheckPositionMouseClick(x, y)) 205. { 206. case ePlay: 207. SetPlay(!m_Section[ePlay].state); 208. if (m_Section[ePlay].state) 209. { 210. RemoveCtrlSlider(); 211. m_Slider.Minimal = iPinPosX; 212. }else CreateCtrlSlider(); 213. break; ... 249. //+------------------------------------------------------------------+
C_Controls.mqhソースコード部分
先ほども述べたように、プログラミングを始めたばかりの方にとっては、今回の解決方法を完全に理解するのは難しいかもしれません。特に、一見するとコードに何の違いもないように見えるためです。というのも、今回の修正は、大量のコードや大きなクラス変更を必要とせず、たった1行の追加で問題が解決されたからです。そう、たった1行です。メモリダンプの問題を解消するには、非常に単純で一見取るに足らない1行のコードを追加する必要がありました。しかしこれは、プログラミング初心者には予測しにくいものです。この小さな変更は、経験のあるプログラマーなら一目でその重要性を理解できるでしょう。しかし、もしその1行が存在しなければ、MetaTrader 5はC_DrawImageクラスに属するオブジェクトが正しくメモリから削除されていないと警告を出します。
ではなぜ、この問題がC_DrawImageではなく、C_Controlsクラスにあるのでしょうか。その理由を見ていきます。前述の通り、C_DrawImageは直接ではなく間接的にアクセスされます。というのも、47行目で宣言されたポインタを介してC_DrawImageにアクセスするよう設計されているからです。C_DrawImageがポインタを通じて参照されることに注意してください。ただし、MQL5 はポインタを C/C++ とは少し異なる方法で処理するため、47行目を単純な変数宣言と考えることができます。それでも、問題はポインタの宣言そのものにはなくポインタの管理方法にあります。
MQL5がC/C++と異なるポインタの扱いを採用している理由の一つに、ポインタに関連する不具合が非常に診断しにくいことが挙げられます。一部のバグは特定の条件でしか発生しないため、原因を突き止めるのが困難です。何度テストしても問題が現れないのに、デバッグしていないときに限って発生する、ということも珍しくありません。それが最悪のパターンです。
そのため、ポインタとして使用する変数は、できるだけ早い段階で初期化するのがベストプラクティスです。これは変数全般にも言えることですが、クラスを扱う場合はコンストラクタで初期化するべきです。実際、144~149行目にその初期化処理が見られます(具体的には148行目でNULLに設定)。
ここからが重要なポイントです。クラスのコンストラクタは、次の2つの方法で呼び出されます。1つ目は、クラスを通常の変数として扱うケース。この場合、コンパイラが自動でメモリを確保するため問題は発生しません。2つ目は、ポインタ経由でクラスを扱う場合。この場合は、new演算子でメモリを確保し、delete演算子で手動で解放する必要があります。
次に、156行目に実装されたC_Controlsクラスのデストラクタを見てください。ここは問題の本質を理解するために非常に重要なので、細心の注意を払って読んでください。この156行目のデストラクタが158行目を実行すると、存在すればC_DrawImageクラスのデストラクタが呼び出されます。ですが、ここで重要なのは、その行が実行されるときに、ポインタによって確保されたメモリも解放されなければならないという点です。これが正しく処理されていれば、MetaTrader 5はメモリリークに関する警告を出すことはありません。つまり、メモリは問題なく正常に解放されるはずです。しかし、実際にはそうなっていません。では、なぜなのでしょうか。
原因は、前のコードのどこかで何かがdelete演算子の動作に干渉していることにあります。そして、そのようなことが可能なのはnew演算子だけです。つまり、問題はnewの使い方にあるのです。では、なぜこのような失敗がここで起きるのでしょうか。それは、MQL5におけるnew演算子の挙動がC/C++にかなり似ているためです。言語によってはnewの実装が異なることもありますが、私の知る限りMQL5はC++に近い動作をします。それでは、ここで実際に何が起こっているのかを詳しく見ていきましょう。
C_DrawImageで管理されているオブジェクトが生成されるとき、それは常にnew演算子を使ってインスタンス化されます。具体的には、以下の行でそれがおこなわれています。
- 75行目:再生/一時停止ボタン用の画像が生成される
- 84~86行目:スライダーコントロール用のボタンが生成される
一方で、C_DrawImageのメモリを明示的に解放しているのは、2箇所しかありません。それが、C_Controlsクラスのデストラクタ内と、95行目です。ここで注目すべき重要なポイントがあります。それは、96行目でdeleteによってメモリを解放した直後に、そのポインタにNULLを代入していることです。なぜそのような処理をするのでしょうか。それは、既に無効となったメモリアドレスを参照すると、ゴミデータを読み込んでしまったり、重要なデータを上書きしてしまったり、さらには悪意あるコードが実行されてしまう危険があるからです。これが、解放済みのポインタには必ずNULLを代入するというのが、広く推奨されているベストプラクティスである理由です。必ずです。しかし、ここではその予防措置を講じているにも関わらず、エラーが発生してしまいました。
ではもう一度、new演算子が使われている箇所を振り返ってみましょう。74行目のコードは、クラスの初期実装時から存在しており、ポインタがすでに使われていない場合にのみ、メモリを確保するようになっています。つまり、この行には問題はありません。ですが、82行目は元々存在していませんでした。では、なぜでしょう。正直言って、私にもわかりません。単に忘れていたのかもしれませんし、大した違いにはならないと思ったのかもしれません。本当に分かりません。ですが、MetaTrader 5がメモリリークを報告してきた原因は、まさにこの82行目がなかったことだったのです。
ポインタがすでに使われているかどうかを確認するという、一見すると些細なチェックが持つ影響力の大きさに、最初は気づかないかもしれません。もしかすると、CreateCtrlSliderメソッドはRemoveCtrlSliderの後にのみ呼ばれる想定だったために、これは単なる見落としではなかったのかもしれません。しかし、その想定が外れるケースがひとつだけ存在します。それが212行目です。ここが、今回の問題の真の発端です。この212行目によって、new演算子がC_DrawImageを繰り返しインスタンス化することになります。そして以前のインスタンスをdeleteせずに新しいメモリを毎回確保してしまうため、オブジェクトが重複していきます。時間が経つにつれて、これらのメモリは解放されずにどんどん積み重なり、最終的にはアプリケーションが終了するまで蓄積され続けます。そしてそのタイミングで、MetaTrader 5が障害を報告するのです。
結論
先ほども述べたように、一部のプログラミング言語では、すでに使用中のポインタに対して新たに別の値を再代入できないようになっています。ですが、少なくとも私の観察した限り、MQL5ではそうした制約はありません。ここで取り上げたのは、MQL5の脆弱性というよりも、プログラムを書く際に注意深く扱うべき性質のものです。MQL5はポインタをサポートしていないと主張する人も多いですが、それは明らかに完全な真実ではありません。そして、ポインタを適切に管理しなければ、実装上の困難が見当たらないような一見単純なプログラムであっても、深刻な問題に直面することになります。
これまでの年月の中で、私はポインタが突如として不安定に振る舞い始めることによって発生する、数え切れないほどのバグに対処してきました。そして、長年の経験を積んだ今になっても、私は再びポインタ関連のエラーに足を取られたのです。それは、82行目に「そのポインタがすでに使用中でないことを確認する」という、たったひとつの簡単なテストを入れなかったからです。また、212行目の呼び出しも見落としていました。そこはマウスイベント内にあり、ある条件下でマウスが動くたびに実行される仕組みになっていました。
この説明が読者の役に立ち、同時に警告となることを心から願っています。ポインタを決して侮ってはいけません。ポインタは非常に強力なツールですが、誤って使えば深刻な問題を引き起こします。以下のビデオでは、問題が修正された後のシステムの動作をご覧いただけます。
デモビデオ
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12250




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