2018年10月11日木曜日

MQL4 で可変長配列を使用する方法

2019/4/12 追記

CArray 系クラスの中で使用されているメモリの動的確保について書きました。
MQL4 で配列用メモリを動的確保する方法 | Strategy of C
場合によっては、こちらを直接使った方が良いかもしれません。

@MetaTrader 4 (Version 4.00 Build 1090)

MQL4 では C++ の std::vector のような動的配列が標準で使用できます1

Include/Arrays/ 以下に定義されており、#include <Arrays/ArrayObj.mqh> のように include するだけで使用可能です。

今回はその中の CArrayObj について簡単な使い方を書こうと思いますが、 CArrayIntCArrayChar 等他の class も基本の思想は同じです。

公式のリファレンスを探したのですが、 MQL5 用のものしか見つけられませんでした。 ただ、確認した限り、MQL4 でも同様に使用できるようです2

参考

Standard Library / Data Collections - Reference on algorithmic/automated trading language for MetaTrader 5

なお、完全に動作するソースコードは最後に載せておきます。

配列に挿入可能な class

CArrayObj に追加可能なオブジェクトは CObject を継承している必要があります。

また、Sort() メソッドを使用したい場合、Compare() を Override しないと正しく動作しないので注意が必要です。 後ほど書きますが Search() 等の一部のメソッドは Sort() 済みでないと動作しません。そのため、そういったメソッドを使用するためにも Compare() は必要です。

7
8
9
10
11
12
13
14
15
16
class Item : public CObject {
    // 省略
 
    // この Compare を「正しく」Orverride していないと
    // Sort() が出来ない。結果 Search() も出来ない。
    virtual int Compare(const CObject *node, const int mode=0) const {
        int diff = this.GetValue() - dynamic_cast<const item*="">(node).GetValue();
        return (diff == 0) ? 0 : (diff > 0) ? 1 : -1;
    }
};

追加、挿入

配列の末尾に追加するには Add(), 途中に挿入するには Insert() を使用します。

37
38
39
40
41
42
43
44
45
// 末尾に追加
for (int i = 0; i < 10; i++) {
    int value = (i % 2 == 0) ? i : i + 10;
    m_item_array.Add(new Item(value));
}   
 
// 途中に挿入
Item* item_100 = new Item(100);
m_item_array.Insert(item_100, 5);

上記を実行した後の配列の状態

1
2
3
4
5
6
7
8
9
10
11
i:0, value:0
i:1, value:11
i:2, value:2
i:3, value:13
i:4, value:4
i:5, value:100
i:6, value:15
i:7, value:6
i:8, value:17
i:9, value:8
i:10, value:19

参考

[MQL4] CArrayObj::Insert() に最大値以上の index を指定すると Add と同じ動作になる

特定位置のオブジェクトを取得する

インデックスがわかっている場合は At() を使います3

1
Item* item = m_item_array.At(i);

あるオブジェクトのインデックスを調べる

特定のオブジェクトが配列に含まれるか、含まれているならどの位置なのか調べるには Search() メソッドを使用します。

この Search()、ソート済みでない配列に対して呼ばれた場合、-1 を返すので注意が必要です。

また、Search() が調べるのは、オブジェクトのリファレンスではなく、Compare() が 0 を返すかどうかです。 そのため、同じ value を持つオブジェクトが複数ある場合、想定とは違った動作になる可能性があります。

53
54
55
56
// 特定の Item の index を調べる
// Sort 済みの配列でないと Search の返り値が -1 になるので注意
m_item_array.Sort();
int index = m_item_array.Search(item_100);

なお、上記の index は 10 になります。ソートしたら value: 100 は最後にくるので。

1
2
index_100
i:10

参考

配列から削除する

配列から削除する方法は大きく分けて2つあります。

1つは単に配列から取り除くだけの Detach()。 配列から消えるだけなので、メモリの解放をしてやらないとメモリリークになります。

58
59
60
61
62
63
64
// index を指定して配列から除く
m_item_array.Detach(index);
 
// Detach は Item を配列から除くだけなので、自前で delete する必要がある
delete item_100;
// delete したら NULL 入れておいた方が安全(今回は無意味だが)
item_100 = NULL;

上記を実行した後の配列の状態。

1
2
3
4
5
6
7
8
9
10
i:0, value:0
i:1, value:2
i:2, value:4
i:3, value:6
i:4, value:8
i:5, value:11
i:6, value:13
i:7, value:15
i:8, value:17
i:9, value:19

もう1つは、配列から除くと同時に delete も行ってくれる Delete()。 こちらの方が便利ですが、別の場所でリファレンスを持ってる場合、重大なエラーになるので注意が必要です。

72
73
74
75
76
for (int i = m_item_array.Total() - 1; i >= 0; i--) {
    Item* item = m_item_array.At(i);
    // Delete は Detach と同時に delete も行ってくれる
    m_item_array.Delete(i);
}

上記を実行すると配列は空になります。 これは、Clear() メソッドを実行するのと等価ですが、今回はあえてループで書いてみました。

全ソースコード

Script/ArrayObjTest.mq4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#property strict
 
#include <Object.mqh>
#include <Arrays/ArrayObj.mqh>
 
// 実際は別ファイルで定義した方が良いでしょう
class Item : public CObject {
private:
    const int m_value;
 
public:
    Item(int value) : m_value(value) {
    }
 
    virtual ~Item() {
    }
 
    int GetValue() const {
        return m_value;
    }
 
    // この Compare を「正しく」Orverride していないと
    // Sort が出来ない。結果 Search も出来ない。
    virtual int Compare(const CObject *node, const int mode=0) const {
        int diff = this.GetValue() - dynamic_cast<const item*="">(node).GetValue();
        return (diff == 0) ? 0 : (diff > 0) ? 1 : -1;
    }
};
 
CArrayObj m_item_array;
 
void OnStart()
{
    int file_handle = FileOpen("result.txt", FILE_TXT|FILE_WRITE);
    // 本当はファイルが開けなかった時のエラーハンドリングが必要だが今回は省略
 
    // 末尾に追加
    for (int i = 0; i < 10; i++) {
        int value = (i % 2 == 0) ? i : i + 10;
        m_item_array.Add(new Item(value));
    }   
 
    // 途中に挿入
    Item* item_100 = new Item(100);
    m_item_array.Insert(item_100, 5);
 
    FileWriteString(file_handle, "============\n");
    for (int i = 0; i < m_item_array.Total(); i++) {
        Item* item = m_item_array.At(i);
        FileWriteString(file_handle, StringFormat("i:%d, value:%d\n", i, item.GetValue()));
    }
 
    // 特定の Item の index を調べる
    // Sort 済みの配列でないと Search できないので注意
    m_item_array.Sort();
    int index = m_item_array.Search(item_100);
 
    // index を指定して配列から除く
    m_item_array.Detach(index);
 
    // Detach は Item を配列から除くだけなので、自前で delete する必要がある
    delete item_100;
    // delete したら NULL 入れておいた方が安全(今回は無意味だが)
    item_100 = NULL;
 
    FileWriteString(file_handle, "============\n");
    for (int i = 0; i < m_item_array.Total(); i++) {
        Item* item = m_item_array.At(i);
        FileWriteString(file_handle, StringFormat("i:%d, value:%d\n", i, item.GetValue()));
    }
 
    for (int i = m_item_array.Total() - 1; i >= 0; i--) {
        Item* item = m_item_array.At(i);
        // Delete は Detach と同時に delete も行ってくれる
        m_item_array.Delete(i);
    }
    // ↑これは m_item_array.Clear(); を実行するのと等価
 
    FileWriteString(file_handle, "============\n");
    FileWriteString(file_handle, StringFormat("Total: %d\n", m_item_array.Total()));
 
    FileClose(file_handle);
}

  1. Build 600 で入った変更を受けて作成された模様 
  2. MQL5 から逆移植されたものと思われる 
  3. 厳密には dynamic_cast を使用するべきだが、こういうケースでは無理に使う必要はないと思う 
?