2018年10月13日土曜日

[MQL4] CArrayObj::Search() が想定外のインデックスを返してくることがある

[MQL4 で可変長配列を使用する方法](https://strategyofc.blogspot.com/2018/10/mql4.html) に書いたのですが、`Sort()` 済みの [CArrayObj](https://www.mql5.com/en/docs/standardlibrary/datastructures/carrayobj) から `Search()` でインデックスを取得しても正しいインデックスを返さない場合があるので注意が必要です。

具体的な例を以下に挙げます。



```mq4
#property strict

#include <Object.mqh>
#include <Arrays/ArrayObj.mqh>

class Item : public CObject {
private:
    const int m_serial;
    const int m_value;
    
public:
    Item(int serial, int value) : m_serial(serial), m_value(value) {
    }
    
    virtual ~Item() {
    }
    
    int GetSerial() const {
        return m_serial;
    }
        
    int GetValue() const {
        return m_value;
    }
    
    // value を比較するので、返り値が 0 になることがある
    virtual int Compare(const CObject *node, const int mode=0) const {
        int diff = this.GetValue() - dynamic_cast(node).GetValue();
        return (diff == 0) ? 0 : (diff > 0) ? 1 : -1;
    }
};

CArrayObj m_item_array;

void OnStart()
{
    for (int i = 0; i < 10; i++) {
        m_item_array.Add(new Item(i, 0)); // value は全部 0
    }    

    // 途中に挿入
    Item* item_10 = new Item(10, 0); // value は 0
    m_item_array.Insert(item_10, 3);
    
    // Sort した後、Search すれば item_10 の場所がわかるはず
    m_item_array.Sort();
    int index = m_item_array.Search(item_10);

    Item* item = m_item_array.At(index);
    printf("index: %d, serial: %d, value: %d", index, item.GetSerial(), item.GetValue());
    
    // 結果
    // 
    // index: 5, serial: 1, value: 0
    //
    // ↑serial は 10 になって欲しいのに、1になってる
}
```

### Search() の結果は Compare() が 0 になるようなオブジェクト
直感的には `Search()` の結果は同じオブジェクトであって欲しいものです。

しかし、`CArrayObj` はそのように実装されていません。
`Compare()` が 0 になるものは同じとみなされます。

これは Quick Sort を使って高速化するためです。
ある意味、しかたのない実装ですが、気をつけて使用しないと重大なバグにつながります。


### Compare() の実装と挿入するオブジェクトに注意
今回のケースの場合、`value` ではなく `serial` を比較すれば問題ありません。
各オブジェクトがユニークかつ比較可能な値を持てる場合はそれを利用するのが良いでしょう。