今天發現一個有趣的問題,以下用保留原始概念、但極度簡化的程式碼重現,所以請高抬貴手不要質疑這麼做的意義何在 。

首先我寫了一個 Table 類別提供 registerItem() 方法,用來註冊物件並取得一個 id 以供後續存取。以下只是示意實作,並沒有處理項目重複之類的問題。

struct Table {
    int registerItem(const string& item) {
        int lastId = itemList_.size();
        itemList_.push_back(item);
        return lastId;
    }

    vector<string> itemList_;
};

然後我想將註冊物件回傳的 id 放到一個 QList 裡面。這裡對 Qt 不熟的網友說明一下,QList 是一個類似 std::list 的容器,提供了重載的 operator<< 作為 append() 和 push_back() 的語法糖。我沒想太多就寫出如下的程式碼,然後很驚訝的發現 idList 的順序與我的預期完全相反,table 裡的順序也是。

    Table table;

    QList<string> idList;
    idList
        << table.registerItem("foo")
        << table.registerItem("bar")
        << table.registerItem("qwerty")
        << table.registerItem("ok")
        ;

    for (auto id : idList) {
        cout << id << ", ";
    }

如果網友想在沒有 Qt 的環境下做實驗,可以把上面的 idList 換成 cout 之類的。

一開始我覺得很奇怪,重載的運算子本質上都是函數,副作用發生的時機點很明確,不應該像原生運算子那樣在 sequence point 之間的副作用順序不定。這個問題困擾了我一下子,為了釐清問題,我把函數呼叫的樹狀圖畫出來,立刻就搞懂怎麼回事。這裡不方便畫圖,以嵌套函數表示。

op(
    op(
        op(
            op(
                idList, 
                table.registerItem("foo")
            ),
            table.registerItem("bar")
        ),
        table.registerItem("qwerty")
    ),
    table.registerItem("ok")
)

這裡的問題在於,C++ 只規定函數所有的參數都必須在呼叫之前演算出來,但是並沒有規定要按照何種順序演算。我在這篇文章 http://novus.pixnet.net/blog/post/26745180 裡也提過這點。

我又嘗試幾種編譯器:

  • clang 3.8
  • g++ 4.9.3
  • g++ 5.4.0
  • VC 19.00.23506

結果只有 clang 的結果合乎預期,不過所有的編譯結果應該都是 C++ 規格允許的。這個經驗告訴我們,重載 operator<< 的語法糖有的時候沒有像表面上那麼可靠。

創作者介紹

novus log

novus 發表在 痞客邦 PIXNET 留言(0) 人氣()