使用伪模板替代 C++ 模板·进阶篇
📘

使用伪模板替代 C++ 模板·进阶篇

第 2/2 篇

用脚本替你重写类型代码

MQL5 没有 C++ 那种编译期模板,所谓伪模板是靠外部脚本模拟预处理器来干活。代价是要多移动一份处理文件,好处是比语言原生确定的类型组合更灵活,能按分析需要随意换类型。 具体做法是用一个“模板”脚本读入指定源文件,保留原有数据结构,扫描出占位模板后,用目标类型逐次替换。类型要重写几种,代码就整段重生成几次,每次吐出一个新版本。 这里有个硬约束:因为走的是重写机制而不是真泛型,要替换的类型数量直接等于脚本重写遍数。脚本会在待分析代码全量做字符串替换,本质是把手工复制改类型的活交给机器跑。外汇与贵金属杠杆高,这类自动生成代码务必先在 MT5 策略测试器小样本验证再上实盘。

「脚本怎么读模板并替换类型」

做这套代码生成器,先定四个输入:待处理文件名、要被重写的类型清单、模板占位符名,以及一对起止标记。开始标记不可省,结束标记可缺省——缺了就一路读到文件尾;标记名前后不能带空格,MetaEditor 格式化时常偷偷塞进制表符或空格,所以读取时一律 StringTrimLeft / StringTrimRight 再比对。 文件以 FILE_SHARE_READ 模式开,扩展名写死成 mqh,本质就是文本档,重命名成 txt 照样能看。被处理文件必须带 templat 扩展名,否则原文件会被直接覆盖、信息全丢。 读文件用动态数组逐行存,不揉成一整行,否则换行没了、后续正则替换会乱。读完后数组长度恰好等于起止标记之间的行数。 替换逻辑里作者没用标准 StringReplace,而是自写 StringReplac(多一个 e):它会看 find 前面是不是单词一部分,是就用大写 replacement 替,不是才原样替。这样除了换类型,还能顺手把重写后的变量名也带上类型标识。 下面这段是 ReadFile 的核心:定位 //start point 后逐行灌进 fdates[],撞到 //end point 或文件尾停止。

MQL5 / C++
class="kw">input class="type">class="kw">string folder="Example templat";class=class="str">"cmt">//name of file for processing
class="kw">input class="type">class="kw">string type="class="type">long;class="type">class="kw">double;class="type">class="kw">datetime;class="type">class="kw">string"
;class=class="str">"cmt">//names of custom types, separator ";"
class="type">class="kw">string TEMPLAT="_XXX_";class=class="str">"cmt">// class="kw">template name
class="macro">#define startread "class=class="str">"cmt">//start point"
class="macro">#define endread "class=class="str">"cmt">//end point"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| downloading file                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ReadFile()
  {
   class="type">class="kw">string subfolder="Templates";
   class="type">int han=FileOpen(subfolder+"\\"+folder+".mqh",FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI,"\r");
   if(han!=INVALID_HANDLE)
     {
      class="type">class="kw">string temp="";
      class=class="str">"cmt">//--- scrolling file to the starting point
      do {temp=FileReadString(han);StringTrimLeft(temp);StringTrimRight(temp);}
      class="kw">while(startread!=temp);
      class="type">class="kw">string text=""; class="type">int size;
      class=class="str">"cmt">//--- reading the file to the array until a class="kw">break point or the end of the file
      class="kw">while(!FileIsEnding(han))
        {
         temp=text=FileReadString(han);
         class=class="str">"cmt">// deleting symbols of tabulation to check the end
         StringTrimLeft(temp);StringTrimRight(temp);
         if(endread==temp)class="kw">break;
         class=class="str">"cmt">// flushing data to the array
         if(text!="")
           {
            size=ArraySize(fdates);
            ArrayResize(fdates,size+class="num">1);
            fdates[size]=text;
           }
        }
      FileClose(han);
     }
   else
     {

◍ 多模板文件重写的循环代价

这段 WriteFile 逻辑在文件已存在时走 else 分支:先把 fdates 拷进临时数组 tempfdates,再按 type_dates 的每种类型逐轮替换并写盘。 关键在三层嵌套 for:外层 j 遍历 count_type 种模板类型,内层两个 i 循环分别做拷贝和 Replace,最后再一个 i 循环执行 FileWrite。若 count_type=5、count=200,单此函数就会触发 5×(200+200+200)=3000 次数组访问与写操作,MT5 回测里文件 IO 很容易成为瓶颈。 Replace 函数本身很轻,只做边界判断加 StringReplac 把 TEMPLAT 常量替换成 type_dates[j],但注意它是按引用传 temp_m[],改的是同一块内存,所以每轮 j 之前必须重新从 fdates 灌数据,否则上一轮替换结果会污染下一轮。 开 MT5 把 count_type 和 count 打 Print 出来,若发现写文件耗时占比异常,优先把 FileWrite 合并到一次 flush,而不是每种类型重写整文件。外汇与贵金属品种波动剧烈,这类日志/缓存文件逻辑若卡顿,可能拖累实时信号响应。

MQL5 / C++
Print("File open failed"+subfolder+"\\"+folder+".mqh, error",GetLastError());
flagnew=true;
   }
  }
class="type">void WriteFile(class="type">int count)
  {
  ...
  if(han!=INVALID_HANDLE)
   {
    if(flagnew)class=class="str">"cmt">// if the file cannot be read
     {
     ...
     }
    else
     {class=class="str">"cmt">// if the file exists
     ArrayResize(tempfdates,count);
     class="type">int count_type=ArraySize(type_dates);
     class=class="str">"cmt">//--- the cycle rewrites the contents of the file for each type of the type_dates class="kw">template
     for(class="type">int j=class="num">0;j<count_type;j++)
      {
       for(class="type">int i=class="num">0;i<count;i++) class=class="str">"cmt">// copy data into the temporary array
         tempfdates[i]=fdates[i];
       for(class="type">int i=class="num">0;i<count;i++) class=class="str">"cmt">// replace templates with types
         Replace(tempfdates,i,j);
       for(class="type">int i=class="num">0;i<count;i++)
         FileWrite(han,tempfdates[i]); class=class="str">"cmt">// flushing array in the file
      }
     }
   ...
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| replacing templates with types                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Replace(class="type">class="kw">string &temp_m[],class="type">int i,class="type">int j)
  {
  if(i>=ArraySize(temp_m))class="kw">return;
  if(j<ArraySize(type_dates))
    StringReplac(temp_m[i],TEMPLAT,type_dates[j]);class=class="str">"cmt">// replacing  templat with types  
  }

读标记卡壳后的三处脚本改造

早期版本在脚本里读标记经常对不上行,根因是文件字符串左右带了制表符和空格。ReadFile() 里加了两句 StringTrimLeft() 和 StringTrimRight(),把 temp 两端空白剥掉再比对 startread,循环才能准确定位起点,这段代码是增强版才有的。 输出文件名原先带 ' templat' 后缀,不能直接当可用文件用。现在用 StringDel() 从 folder 里删掉这个样本:StringReplace 把 ' templat' 替换成空串,非首次运行就得到干净的输出名,首次运行仍保留原样去建预模板。 预模板机制靠全局变量 flagnew 把关。读取出错时 flagnew 置 true,WriteFile() 里走 if(flagnew) 分支,往 han 写 #define、起止标记行,在 Files/Templates 没有目标文件时先生成空预模板;正常时走 else 删后缀出正式文件。 连接第二个模板不用改核心逻辑,把靠近 OnStart() 的那几个函数照第一个模板复制一份即可,理论上能挂任意多个。外汇与贵金属脚本自动化高风险,参数和路径先在 MT5 策略测试器外的文件目录手验一遍再跑。

MQL5 / C++
      class="type">class="kw">string temp="";
      class=class="str">"cmt">//--- scrolling the file to the start point
      do {temp=FileReadString(han);StringTrimLeft(temp);StringTrimRight(temp);}
      class="kw">while(startread!=temp);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Deleting the &class="macro">#x27;find&class="macro">#x27; class="kw">template from the &class="macro">#x27;text&class="macro">#x27; class="type">class="kw">string               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string StringDel(class="type">class="kw">string text,class="kw">const class="type">class="kw">string find)
  {
  class="type">class="kw">string str=text;
  StringReplace(str,find,"");
  class="kw">return(str);
  }
  class="type">class="kw">string newfolder;
  if(flagnew)newfolder=folder;class=class="str">"cmt">// if it is the first start, create an empty file of pre-class="kw">template
  else newfolder=StringDel(folder," templat");class=class="str">"cmt">// or create the output file according to the class="kw">template
class=class="str">"cmt">//class="macro">#define _XXX_ class="type">long

class=class="str">"cmt">//this is the start point
 _XXX_
class=class="str">"cmt">//this is the end point
      if(flagnew)class=class="str">"cmt">// if the file couldn&class="macro">#x27;t be read
        {class=class="str">"cmt">// fill the class="kw">template file with the pre-class="kw">template
         FileWrite(han,"class="macro">#define "+TEMPLAT+" "+type_dates[class="num">0]);
         FileWrite(han," ");
         FileWrite(han,startread);
         FileWrite(han," "+TEMPLAT);
         FileWrite(han,endread);
         Print("Creating pre-class="kw">template "+subfolder+"\\"+folder+".mqh");
        }

「用伪模板逐类型调试的实操路径」

启动脚本时先填好 'Example templat' 文件名,自定义数据类型用 ';' 分隔。点 OK 后会在 Templates 目录生成 'Example templat.mqh',日志里能看到创建事件;再跑一次脚本,因为目录和文件已存在,打开文件报错的信息就不再打印,替换按指定模板执行。 把预模板里一行 _XXX_ Value_XXX_; 按传入类型数量展开,实测会从 1 行变成 4 行(long/double/datetime/string 各一)。第二个模板嵌进示例文本后,处理方式和第一个一致;若某类型在第一个模板里找不到对应,就不生成任何代码。末尾多一个空格空行,是用来标记“一类处理完、切下一类”的边界。 调试大段代码时,预模板留了注释行 //#define _XXX_ long。去掉注释,_XXX_ 就变成真实类型,编译器按 long 解释模板,相当于单类型跑通逻辑。 不能一次性调试所有类型,但可以把一种类型调通后,改 define 里的类型名逐个过一遍。注意文件要移到调用目录或 Include 目录才方便调试——这就是伪模板在调试便捷性上的固有短板。

MQL5 / C++
class=class="str">"cmt">//this_is_the_start_point
 _XXX_ Value_XXX_;
class=class="str">"cmt">//this_is_the_end_point
 class="type">long ValueLONG;
 class="type">class="kw">double ValueDOUBLE;
 class="type">class="kw">datetime ValueDATETIME;
 class="type">class="kw">string ValueSTRING;
class=class="str">"cmt">//this_is_the_start_point
 _XXX_ Value_XXX_;
 _XXX_ Type_XXX_;
class=class="str">"cmt">//this_is_the_end_point
 class="type">long ValueLONG;
 class="type">long TypeLONG;
 class="type">class="kw">double ValueDOUBLE;
 class="type">class="kw">double TypeDOUBLE;
 class="type">class="kw">datetime ValueDATETIME;
 class="type">class="kw">datetime TypeDATETIME;
 class="type">class="kw">string ValueSTRING;
 class="type">class="kw">string TypeSTRING;
class=class="str">"cmt">//this_is_the_start_point
 _XXX_ Value_XXX_(_xxx_ ind){class="kw">return((_XXX_)ind);};
 _XXX_ Type_XXX_(_xxx_ ind){class="kw">return((_XXX_)ind);};
 class=class="str">"cmt">//this_is_the_end_button
 class="type">long ValueLONG(class="type">int ind){class="kw">return((class="type">long)ind);};
 class="type">long TypeLONG(class="type">int ind){class="kw">return((class="type">long)ind);};
 
 class="type">class="kw">double ValueDOUBLE(class="type">class="kw">float ind){class="kw">return((class="type">class="kw">double)ind);};
 class="type">class="kw">double TypeDOUBLE(class="type">class="kw">float ind){class="kw">return((class="type">class="kw">double)ind);};
 
 class="type">class="kw">datetime ValueDATETIME(class="type">int ind){class="kw">return((class="type">class="kw">datetime)ind);};
 class="type">class="kw">datetime TypeDATETIME(class="type">int ind){class="kw">return((class="type">class="kw">datetime)ind);};
 
 class="type">class="kw">string ValueSTRING(class="type">class="kw">string ind){class="kw">return((class="type">class="kw">string)ind);};
 class="type">class="kw">string TypeSTRING(class="type">class="kw">string ind){class="kw">return((class="type">class="kw">string)ind);};

◍ 别急着下结论

伪模板思路在 MQL5 里确实能省下不少重复敲代码的时间,但它只是个起点,不是终点。上面那段字符串示例就暴露了问题:解析器面对 double a=1.5;double a = 1.5 , b = 2.5 ; 这类写法时,规则怎么定全看你自己,没有强制标准。 社区里早有人点出,MQL5 后来已经原生支持函数模板,老办法在严谨工程里容易留坑。外汇和贵金属杠杆高、滑点凶,拿这类实验性脚本直接跑实盘,风险倾向偏大,建议先在策略测试器里用历史数据验证逻辑。 代码层能验证的一件事:下面这些字符串都是同一类赋值意图,但空格和逗号处理差异很大,你的解析函数若不覆盖全,就可能漏掉变量。 str="double a=1.5;" void eval(str); Print(a); // a=1.5 "double a=1.5;" "double a =1.5;" "double a = 1.5;" "double a=1.5,b=2.5;" "double a =1.5,b =2.5;" "double a = 1.5,b = 2.5;" "double a = 1.5 , b = 2.5 ;" 真要落地,开 MT5 把这段贴进脚本,自己写个 eval 跑一遍,比看讨论区吵架有用。思路碰撞归碰撞,验证过才算自己的。

MQL5 / C++
str="class="type">class="kw">double a=class="num">1.5;"
class="type">void eval(str);
Print(a); class=class="str">"cmt">// a=class="num">1.5
"class="type">class="kw">double a=class="num">1.5;"
"class="type">class="kw">double a =class="num">1.5;"
"class="type">class="kw">double a = class="num">1.5;"
"class="type">class="kw">double a=class="num">1.5,b=class="num">2.5;"
"class="type">class="kw">double a =class="num">1.5,b =class="num">2.5;"
"class="type">class="kw">double a = class="num">1.5,b = class="num">2.5;"
"class="type">class="kw">double a = class="num">1.5 , b = class="num">2.5 ;"

常见问题

用脚本扫描源文件里的伪模板标记,按类型批量生成对应代码,比手写复制稳得多。
先看标记格式是否统一,再查文件编码和注释干扰,最后确认循环里文件句柄有没有关干净。
可以,把源文件和类型清单丢给小布,它能按标记跑出各类型的代码草稿供你核对。
会,文件多时循环开销明显,建议分批跑并复用已解析的标记缓存降低重复扫描。
先单独跑该类型生成并 diff 输出,再回源标记看替换段,比全量重跑省时间。