ジグザグおよび ATR例によるクラスとしてのインディケータ実装

Aleksandr Chugunov | 29 10月, 2015


それは何のために必要なのでしょうか?

MetaQuotes ソフトウェア社は MetaTrader クライアント端末の新しい第5版においてカスタムインディケータとの連携コンセプトを改定しました。現在それらはずっと速く実行されます。ユニークな入力パラメータを持つそれぞれのインディケータの唯一の例があります。よってあるシンボルのチャート10件にそれのコピーを使ったとしても計算は一度ですむのです。

あるアルゴリズムの処理には変更はありません。サーバーや重要な履歴同期 との連携を失うと、値 prev_calculated(または MetaTrader 4でのIndicatorCounted())はゼロになります。こうなると履歴全体のインディケータの再計算が必要になります。(あらゆる状況においてインディケータ値が正確に計算されるのを保証するため開発者が意図的にこうしています。)インディケータの計算スピードに影響を与える事柄は複数あります。

他にもあてはまる状況もあるでしょう。履歴全体についてインディケータを再計算する問題はより現実的な問題です。また、情報変換チャネルが劣悪だと問題はさらに悪くなります。

もちろん、追加の入力パラメータによってインディケータ計算の深さに制限をもたせることも可能ですが、インディケータiCustom 使用時には微妙なものがあります。あらゆるチャートやカスタムインディケータによって使用されるバーの最大数はターミナル全体に対してグローバルスコープ上に設定されます。メモリはカスタムインディケータの各バッファに割り当てられ、TERMINAL_MAXBARSのみがそれに制限を加えます。

ただし、重要な追加事項があります。インディケータのアルゴリズム内で計算したバーの最大数に制限を加えると(たとえば入力パラメータを使用したりコードに直接)、メモリは新規バーが作成される際動的に割り当てられます。(指定の制限 TERMINAL_MAXBARS に対して徐々に増加(またはもう少し増加します。このアルゴリズムは開発者に完全に依存し、次回構築時に開発者がそれを変更することは可能です。))


全履歴に対するインディケータ再計算を避ける方法

いまのところ、この問題解決には以下の方法を考えています。
  1. MetaQuotes にプラットフォームレベルでこの問題を改定するよう依頼する。
  2. prev_calculatedの類似体を実装するために別クラスを作成します。

prev_calculatedを計算するアルゴリズムをインディケータそのものにビルトできるとして別のバリアントがありました。が MetaTrader 5は MetaTrader 4と異なり、 prev_calculated がゼロになる際、インディケータバッファをすべて「消去」してしまうようです。(すなわち全インディケータ配列を強制的にゼロにします。これはプラットフォームレベルで取り入れられているのでコントロールは不可能です。)

では、各バリアントを個別に分析していきます。


問題解決の第二バリアントのメリット、デメリット

メリット
デメリット


prev_calculatedの類似体実装のためのCCustPrevCalculated クラス作成

クラスの実装自体はなんら面白みはありません。アルゴリズムは履歴を双方向へ広げることと、左側を「切り捨てる」可能性を考えます。また、このアルゴリズムは計算済みデータの内側に履歴を挿入します。(MetaTrader 4では実際にそうなっています。MetaTrader 5 ではまだ見たことはありません。)クラスのソースコードはファイル CustPrevCalculated.mqhにあります。

重要な点について述べます。


配列エレメントへのリングアクセス作成

このクラスを作成するには、コンベンショナル集約ではない方法を使います。すなわち、配列にメモリを一度に割り当てるのと配列コピーの過剰な手順を避けるためのリングアクセスです。5つのエレメントを例にとりそれを考察します。


配列エレメントへのリングアクセス


 
まず、列挙が0から始まる配列について作業します。さて、配列サイズを保ったまま(新規バー追加)次の値を追加するのに必要なものは何でしょうか?方法は2つあります。

二番目のバリアント実装のために、変数が一つ必要です。それを DataStartIndと名づけます。この変数は配列のゼロインデックスの位置を格納します。今後の計算がしやすいように、その列挙は配列の通常インデックスに対応させます。(すなわちそれはゼロから始まります。)BarsLimit 変数には、配列のエレメント数を格納します。よって、仮想インデックス 'I' に対する配列エレメントの実アドレスは以下の簡単な式を使って計算されます。

変数DataBarsCount variableは実際に使用されるメモリセル数を格納します。(たとえば、セル5つのうち3つだけの使用が可能です。)


履歴同期のアルゴリズム

私自身は、クライアント端末の履歴を伴う履歴コピー(ローカル履歴)の同期アルゴリズムの動作モデルを3種類選択し実装しました。

同期のメカニズム自体はプログラマーによる別のパラメータセットに基づいて行われます。それはHSMinute(HistorySynchSecondとして格納されます。)です。「ディーラーセンター」は履歴の最終 HSMinute 分のみ修正すると考えられます。その期間の同期中に相違が見られなければ、履歴は同一とみなされ、比較は停止します。違いが見つかれば、全履歴がチェックされ修正されます。

その上、このアルゴリズムは初期化の際に指定されたストラクチャ MqlRatesからの価格、スプレッド、ボリュームのみ確認します。たとえば、ZigZagを描画するのに必要なのは高値と安値のみです。


クラスCCustPrevCalculatedの実用

CCustPrevCalculated クラスを初期化するには、問題がない場合に 'true' を返す関数InitData()を呼ぶ必要があります。
CCustPrevCalculated CustPrevCalculated;
CustPrevCalculated.InitData(_Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15);
履歴を同期するためには関数 PrepareData()を呼ぶ必要があります。
CPCPrepareDataResultCode resData;
resData = CustPrevCalculated.PrepareData();

PrepareData() 関数によって返されるバリアント値

enum CPCPrepareDataResultCode
  {
   CPCPDRC_NoData,                     // Returned when there is no data for calculation (not prepared by the server)
   CPCPDRC_FullInitialization,         // Full initialization of the array has been performed
   CPCPDRC_Synch,                      // Synchronization with adding new bars has been performed
   CPCPDRC_SynchOnlyLastBar,           // Synchronization of only the last bar has been performed (possible cutting of the history)
   CPCPDRC_NoRecountNotRequired        // Recalculation has not been performed, since the data was not changed
  };


データアクセスのためのClass CCustPrevCalculated クラス関数

注意:計算速度を上げるために、配列オーバーフローチェックは省きます。精度を上げるために、インデックスが正しくなければ誤った値は返されます。

名前
用途
 uint GetDataBarsCount()
 変数バー数を返します。
 uint GetDataBarsCalculated()
 変更のないバー数を返します。
 uint GetDataStartInd()
 ラップアラウンドアクセス(カスタムインディケータへの)用インデックスを返します。
 bool GetDataBarsCuttingLeft()
 左側からのバー切り捨て結果を返します。
 double GetDataOpen(int shift, bool AsSeries)
 移動バーに対する「オープン」を返します。
 double GetDataHigh(int shift, bool AsSeries)
 移動バーに対する「高値」を返します。
 double GetDataLow(int shift, bool AsSeries)
 移動バーに対する「安値」を返します。
 double GetDataClose(int shift, bool AsSeries)
 移動バーに対する「クローズ」を返します。
 datetime GetDataTime(int shift, bool AsSeries)
 移動バーに対する「時刻」を返します。
 long GetDataTick_volume(int shift, bool AsSeries)
 移動バーに対する「ティックボリューム」を返します。
 long GetDataReal_volume(int shift, bool AsSeries)
 移動バーに対する「実ボリューム」を返します。
 int GetDataSpread(int shift, bool AsSeries)
 移動バーに対する「スプレッド」を返します。


クラス CCustPrevCalculatedの将来の最適化例


CCustPrevCalculated クラスのデータに基づくZigZagカスタムインディケータ計算のためのCCustZigZagPPC作成

このアルゴリズムはカスタムインディケータProfessional ZigZag を基にしています。このクラスのソースコードはファイル ZigZags.mqh にあります。またライブラリOutsideBar.mqh は外部バーと連携して使われます。

われわれのインディケータのバー記述に個別のストラクチャを作成します。

struct ZZBar
  {
   double UP, DN;                      // Buffers of the ZigZag indicator
   OrderFormationBarHighLow OB;       // Buffer for caching of an external bar
  };

また、クラス計算の戻り結果を判断します。

enum CPCZZResultCode
  {
   CPCZZRC_NotInitialized,             // Class is no initialized
   CPCZZRC_NoData,                     // Faield to receive data (including the external bar)
   CPCZZRC_NotChanged,                 // No changes of ZZ rays
   CPCZZRC_Changed                     // ZZ rays changed
  };

CCustZigZagPPC クラスを初期化するためには Init() 関数を一度呼ぶ必要があります。問題なければそれは 'true' を返します。

CCustZigZagPPC ZZ1;
ZZ1.Init(CustPrevCalculated, _Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15, 0, true, 12, 10);

インディケータ計算のためには CCustPrevCalculatedクラスの前回の計算データを基にした更新データを開始する必要があります。

CPCPrepareDataResultCode resZZ1;
resZZ1 = ZZ1.PrepareData(resData);

そしてプロシージャ Calculate()を呼びます。

if ( (resZZ1 != CPCPDRC_NoData) && (resZZ1 != CPCPDRC_NoRecountNotRequired) )
   ZZ1.Calculate();

クラスCCustPrevCalculated を複数の CCustZigZagPPC クラスと共に使用する用例全体はファイル ScriptSample_CustZigZagPPC.mq5にあります。


クラスCCustZigZagPPCのデータアクセス関数

名前
用途
 uint GetBarsCount()
 変数バー数を返します。
 uint GetBarsCalculated()  計算されたバー数を返します。
 double GetUP(uint shift, bool AsSeries)
 バーに対するZigZagピーク値を返します。
 double GetDN(uint shift, bool AsSeries)
 バーに対するZigZag最小値を返します。
 OrderFormationBarHighLow GetOB(uint shift, bool AsSeries)  バーに対する「外部」値を返します。


ビジュアルチェックとプログラムチェック

ビジュアルチェックのために、元のインディケータをチャートにアタッチし、その上に同じ入力パラメータ(両インディケータを確認するために別の色を選択する必要があります。)を伴う特別に書かれた検証用インディケータ Indicator_CustZigZag.mq5 をアタッチします。

赤:元のインディケータ、ブルー:最終100バーについて計算されるここでのインディケータ

同じようにExpert Advisorも比較します。なにか違いがありますか?クラスiCustom("AlexSTAL_ZigZagProf") および CCustZigZagPPC から得られる結果は検証用Expert Advisor のExpert_CustZigZagPPC_test.mq5内で毎ティックごとに比較されます。計算に関する情報はジャーナルに表示されます。(最初のバーでは計算は行われません。なぜならアルゴリズム用の履歴が存在しないからです。)

(EURUSD,M1)                1.35797; 1.35644; 1.35844; 1.35761; 1.35901; 1.35760; 1.35959; 1.35791; 1.36038; 1.35806; 1.36042; 1.35976; 1.36116; 1.35971; // it is normal
(EURUSD,M1) Tick processed: 1.35797; 1.35644; 1.35844; 1.35761; 1.35901; 1.35760; 1.35959; 1.35791; 1.36038; 1.35806; 1.36042; 1.35976; 1.36116; 
(EURUSD,M1) Divergence on the bar: 7 

このExpert Advisor についてもっと詳しく考察します。動作の グローバル変数 を決めます。

#include <ZigZags.mqh>

CCustPrevCalculated CustPrevCalculated;
CCustZigZagPPC ZZ1;
int HandleZZ;

変数を初期化します。

int OnInit()
  {
   // Creating new class and initializing it
   CustPrevCalculated.InitData(_Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15);
   
   // Initializing the class ZZ
   ZZ1.Init(GetPointer(CustPrevCalculated), _Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15, 0, true, 12, 10);
   
   // Receiving handle for the custom indicator
   HandleZZ = iCustom(_Symbol, _Period, "AlexSTAL_ZigZagProf", 12, 10, 0 , true);
   Print("ZZ_handle = ", HandleZZ, "  error = ", GetLastError());

   return(0);
  }
ティックをExpert Advisorで処理します。
void OnTick()
  {
   // Calculation of data
   CPCPrepareDataResultCode resData, resZZ1;
   resData = CustPrevCalculated.PrepareData();
   
   // Start recalculation for each indicator! PrepareData obligatory!
   resZZ1 = ZZ1.PrepareData(resData);
   
   // Расчет данных ZZ1
   if ( !((resZZ1 != CPCPDRC_NoData) && (resZZ1 != CPCPDRC_NoRecountNotRequired)) )
      return;

   // Получим результаты расчета
   ZZ1.Calculate();

CCustZigZagPPCによって計算されたバーZZ1.GetBarsCalculated()を取得しました。iCustomデータ("AlexSTAL_ZigZagProf") とCCustZigZagPPCクラスをデータ比較するコードを追加します。

   int tmpBars = (int)ZZ1.GetBarsCalculated();
   double zzUP[], zzDN[];
   CopyBuffer(HandleZZ, 0, 0, tmpBars, zzUP);
   CopyBuffer(HandleZZ, 1, 0, tmpBars, zzDN);
   
   // Perform comparison
   string tmpSt1 = "", tmpSt2 = "";
   for (int i = (tmpBars-1); i >= 0; i--)
     {
      double tmpUP = ZZ1.GetUP(i, false);
      double tmpDN = ZZ1.GetDN(i, false);
      if (tmpUP != zzUP[i])
         Print("Divergence on the bar: ", i);
      if (tmpDN != zzDN[i])
         Print("Divergence on the bar: ", i);
      if (tmpUP != EMPTY_VALUE)
         tmpSt1 = tmpSt1 + DoubleToString(tmpUP, _Digits) + "; ";
      if (tmpDN != EMPTY_VALUE)
         tmpSt1 = tmpSt1 + DoubleToString(tmpDN, _Digits) + "; ";

      if (zzUP[i] != EMPTY_VALUE)
         tmpSt2 = tmpSt2 + DoubleToString(zzUP[i], _Digits) + "; ";
      if (zzDN[i] != EMPTY_VALUE)
         tmpSt2 = tmpSt2 + DoubleToString(zzDN[i], _Digits) + "; ";
     }
  Print("Tick processed: ", tmpSt1);
  Print("                              ", tmpSt2);
  }

ここに Expert Advisor またはスクリプトでのCCustZigZagPPCクラスの簡単な活用法があります。CopyBuffer()の代わりの直接アクセス関数 GetUP()、 GetDN()、GetOB()


インディケータの個別クラスへの移動(iATRの例により)

ファイル ZigZags.mqh を基に、前述の原則に従ったカスタムインディケータをすばやく作成するテンプレートMyIndicator.mqh を作成しました。

通常プラン

1. 準備段階

2. クラスに対する初期(元の)インディケータから取られる外部パラメータを選択し、それらを初期化することを宣言します。

この例ではATR インディケータがひとつの外部パラメータを持ちます。
input int InpAtrPeriod=14;  // ATR period
class CCustATR
  {
protected:
   ...
   uchar iAtrPeriod;
   ...
public:
   ...
   bool Init(CCustPrevCalculated *CPC, string Instr, ENUM_TIMEFRAMES TF, int Limit, CPCHistorySynchMode HSM, uchar HS, uint HSMinute, uchar AtrPeriod);
bool CCustATR::Init(CCustPrevCalculated *CPC, string Instr, ENUM_TIMEFRAMES TF, int Limit, CPCHistorySynchMode HSM, uchar HS, uint HSMinute, uchar AtrPeriod)
{
      ...
      BarsLimit = Limit;
      iAtrPeriod = AtrPeriod;
      ...

3. 初期インディケータで要求するバッファ数を決め、クラスで宣言します。またINDICATOR_DATA バッファを返す関数を宣言します。

struct ATRBar
  {
   double Val;                          // Indicator buffers
  };

独自のストラクチャへ

struct ATRBar
  {
   double ATR;
   double TR;
  };
CPCPrepareDataResultCode CCustATR::PrepareData(CPCPrepareDataResultCode resData)
{
   ...
   for (uint i = (DataBarsCalculated == 0)?0:(DataBarsCalculated+1); i < DataBarsCount; i++)
     {
      Buf[PInd(i, false)].ATR = EMPTY_VALUE;
      Buf[PInd(i, false)].TR = EMPTY_VALUE;
     }
   ...

以下から

class CCustATR
  {
   ...
   double GetVal(uint shift, bool AsSeries);                      // returns the Val value of the buffer for a bar
   ...

に変更します。(バッファが一つしかなければ変更手順はとばしてかまいません。)

class CCustATR
  {
   ...
   double GetATR(uint shift, bool AsSeries);                      // Возвращает значение буфера ATR для бара
   ...

そして対応する関数のコードを変更します。

double CCustATR::GetATR(uint shift, bool AsSeries)
{
   if ( shift > (DataBarsCount-1) )
      return(EMPTY_VALUE);
   return(Buf[PInd(shift, AsSeries)].ATR);
}
注意:バッファ値を返す複数の関数の代わりに追加のパラメータであるバッファ数またはバッファ名を持つ関数を一つだけ使用することも可能です。


4. クラスの対応する関数に対して初期インディケータ関数 OnCalculate() こロジックをコピーします。

CPCATRResultCode CCustATR::Calculate()
{
   ...
   // Check if there are enough bars for the calculation
   if (DataBarsCount <= iAtrPeriod)
      return(CPCATRRC_NoData);
   ...
   if ( DataBarsCalculated != 0 )
      BarsForRecalculation = DataBarsCount - ATRDataBarsCalculated - 1;
   else
     {
      Buf[PInd(0, false)].TR = 0.0;
      Buf[PInd(0, false)].ATR = 0.0;
      //--- filling out the array of True Range values for each period
      for (uint i = 1; i < DataBarsCount; i++)
         Buf[PInd(i, false)].TR = MathMax(CustPrevCalculated.GetDataHigh(i, false), CustPrevCalculated.GetDataClose(i-1, false)) - 
                                  MathMin(CustPrevCalculated.GetDataLow(i, false), CustPrevCalculated.GetDataClose(i-1, false));
      //--- first AtrPeriod values of the indicator are not calculated
      double firstValue = 0.0;
      for (uint i = 1; i <= iAtrPeriod; i++)
        {
         Buf[PInd(i, false)].ATR = 0;
         firstValue += Buf[PInd(i, false)].TR;
        }
      //--- calculating the first value of the indicator
      firstValue /= iAtrPeriod;
      Buf[PInd(iAtrPeriod, false)].ATR = firstValue;
      
      BarsForRecalculation = DataBarsCount - iAtrPeriod - 2;
     }
   for (uint i = (DataBarsCount - BarsForRecalculation - 1); i < DataBarsCount; i++)
     {
      Buf[PInd(i, false)].TR = MathMax(CustPrevCalculated.GetDataHigh(i, false), CustPrevCalculated.GetDataClose(i-1, false)) - 
                               MathMin(CustPrevCalculated.GetDataLow(i, false), CustPrevCalculated.GetDataClose(i-1, false));
      Buf[PInd(i, false)].ATR = Buf[PInd(i-1, false)].ATR + (Buf[PInd(i, false)].TR-Buf[PInd(i-iAtrPeriod, false)].TR) / iAtrPeriod;
      ...

以上です。われわれのクラスが作成されました。目で確認するためにテストインディケータ(今回の例ではIndicator_ATRsample.mq5)を作成することも可能です。



本稿を修正している間に思いついたことがあります。それはただ一つのカスタムインディケータをCCustPrevCalculated クラスと共に使用すると、カスタムインディケータ(この例ではCCustZigZagPPC および CCustATR)内でのこのクラスの作成、初期化、同期を統合できる、というものです。この目的のためにカスタムインディケータの初期化関数を呼びだすとき、オブジェクトに対するゼロポインターの使用が必要です。

   ATR.Init(NULL, _Symbol, _Period, iBars, CPCHSM_Normal, 0, 30, InpAtrPeriod);

一般的ストラクチャでは

#include <CustPrevCalculated.mqh>
#include <ATRsample.mqh>
CCustPrevCalculated CustPrevCalculated;
CCustATR ATR;

int OnInit()
  {
   CustPrevCalculated.InitData(_Symbol, _Period, iBars, CPCHSM_Normal, 0, 30);
   ATR.Init(GetPointer(CustPrevCalculated), _Symbol, _Period, iBars, CPCHSM_Normal, 0, 30, InpAtrPeriod);
  }

int OnCalculate(...)
  {
   CPCPrepareDataResultCode resData = CustPrevCalculated.PrepareData();
   CPCPrepareDataResultCode resATR = ATR.PrepareData(resData);
   if ( (resATR != CPCPDRC_NoData) && (resATR != CPCPDRC_NoRecountNotRequired) )
      ATR.Calculate();
  }

が以下を簡素化するでしょう。

#include <ATRsample.mqh>
CCustATR ATR;

int OnInit()
  {
   ATR.Init(NULL, _Symbol, _Period, iBars, CPCHSM_Normal, 0, 30, InpAtrPeriod);
  }

int OnCalculate(...)
  {
   ATR.Calculate();
  }
実用例はファイルIndicator_ATRsample2.mq5にあります。

ストラテジーテスタパフォーマンスに記述されたテクノロジーの影響

確認のため、3つのバリアントのうちの一つに従いティックごとにゼロバーインディケータ値を受け取る検証用Expert Advisor (TestSpeed_IndPrevCalculated.mq5) を作成しました。

enum eTestVariant
  {
   BuiltIn,    // Built-in indicator iATR
   Custom,     // Custom indicator iCustom("ATR")
   IndClass    // Calculation in the class
  };

このExpert Advisor は以下の最適化パラメータで1エージェントにつき10回実行しました。

インディケータの3つのバリアントのうちの一つを使ったときの最適化時間を計測しました。チェック結果は線形ヒストグラムをして表示します。

ATR インディケータの3タイプの実装に対する最適化時間

以下は最適化時間計測に使用されたExpert Advisor のソースコードです。

//+------------------------------------------------------------------+
//|                                  TestSpeed_IndPrevCalculated.mq5 |
//|                                         Copyright 2011, AlexSTAL |
//|                                           http://www.alexstal.ru |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, AlexSTAL"
#property link      "http://www.alexstal.ru"
#property version   "1.00"
//--- connect the include file with the CustATR class
#include <ATRsample.mqh>
//--- set the selection of the parameter as an enumeration
enum eTestVariant
  {
   BuiltIn,    // Built-in indicator iATR
   Custom,     // Custom indicator iCustom("ATR")
   IndClass    // Calculation withing the class
  };
//--- input variables
input eTestVariant TestVariant;
input int          FalseParameter = 0;
//--- period of the ATR indicator
const uchar        InpAtrPeriod = 14;
//--- handle of the built-in or custom indicator
int                Handle;
//--- indicator based on the class 
CCustATR           *ATR;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //---
   switch(TestVariant)
     {
      case BuiltIn:
         Handle = iATR(_Symbol, _Period, InpAtrPeriod);
         break;
      case Custom:
         Handle = iCustom(_Symbol, _Period, "Examples\ATR", InpAtrPeriod);
         break;
      case IndClass:
         ATR = new CCustATR;
         ATR.Init(NULL, _Symbol, _Period, 100, CPCHSM_Normal, 0, 30, InpAtrPeriod);
         break;
     };
   //---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   switch(TestVariant)
     {
      case IndClass:
         delete ATR;
         break;
     };
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   double tmpValue[1];
   switch(TestVariant)
     {
      case BuiltIn:
         CopyBuffer(Handle, 0, 0, 1, tmpValue);
         break;
      case Custom:
         CopyBuffer(Handle, 0, 0, 1, tmpValue);
         break;
      case IndClass:
         ATR.Calculate();
         tmpValue[0] = ATR.GetATR(0, true);
         break;
     };
  }
//+------------------------------------------------------------------+

見てのとおり、このテクノロジーは通常のカスタムインディケータに比べストラテジーテスタのパフォーマンスをそれほど落とすものではありません。


テクノロジーの実用に関する注意


おわりに

それぞれの状況でプログラマーはタスクに実装する異なるバリアントの利点と欠点に配慮する必要があります。ここで提案した実装にはそれ自体のメリット、デメリットがあります。

追記失敗をしない者は何もしていないのと同じだ!誤りをみつけたら、ご一報ください。