Sorting std::unique_ptr in associative containers

在工作時因為 associative container 遇到的 bug。在這裡分享一下 XD

使用 associative container 做 resource management

在撰寫管理元素,作為集合功能的物件時,為了 resource management,會讓元素以 unique_ptrCollection 所有。與 Collection 互動的其他結構,可以呼叫它提供的 API 來操作裡頭蒐集的物件。另外為了方便加入與刪除,會使用 associative container (例如 std::set 或是 std::map )。

template<T>
class Collection {
public:
  using CollectionT = std::set<std::unique_ptr<T>>;
  using iterator = CollectionT::iterator;
  using const_iterator = CollectionT::const_iterator;
public:
  iterator begin();
  iterator end();
  const_iterator begin() const;
  const_iterator end() const;
  const_iterator cbegin() const;
  const_iterator cend() const;  
private:
  CollectionT collection;
public:
  /* more utilities ... */
};

為了讓 Collection 與 STL container 一樣可以使用 range-based for ,因此會幫它 overload iterator 們。這時 Collection 作為 STL container 的 wrapper 之後,不小心就讓錯誤發生了!

造成 Non-deterministic 的執行結果

使用 Collection 的人如果下意識覺得這是一個 sequential container,假設 container 是照著「固定的順序」來遍歷在裡頭的物件。這時 std::set 持有 unique_ptr ,會使得它根據記憶體位置大小來排序。而這件事情會讓程式執行變成 non-deterministic ,因為配置記憶體位置是由作業系統來負責,在不同的環境之下會產生不一樣的結果。

檢討

有 bug 就代表寫出來的 code 不足以表達所有資訊,造成合作之間的誤會。當然要來檢討一下。


提供順序性。為 collection 構造 compare class 即可。不過要注意的是,如果你為底下的物件 class T 去構造比較函數的話不會有效果,因為容器持有的物件是經過 std::unique_ptr wrapper 的物件。因此做 template specialization 時要比較的是 unique_ptr<T>

以下以 int 作為舉例。這樣就可以為持有 unique_ptr 的 associative container 構造順序性了。有辦法構造順序性,就可以依據底下元素的特性來排序,避免容器使用記憶體位置來排序。

template<> struct std::less<unique_ptr<int>>
{
  bool operator()(const unique_ptr<int>& a, const unique_ptr<int>& b) const {
    return *a < *b;
  }
};

改進命名。Class 會在最剛開把使用到的容器以及型態做 type alignment。但是上面那個例子中,命名為 Collection 並不適當。因為他沒有表達這個容器是 sequential 還是 associative。簡單的命名為 SetT 都會比較好,至少使用者可以馬上知道現在操作的容器為 std::set,而不會有剛剛誤用成 sequential container 的事發生。

檢討的檢討

不過這裡要繼續討論下去。剛剛的檢討中提出了兩種做法。第一種為了「解決使用者誤會(忘記)這是一個 sequential 容器」而改變了底層容器的構造。這其實是一種 XY Problem。為了解決「想要以固定(deterministic)的方式存取該集合內的物件」而改變原本單純作為「集合」的物件,額外定義出「順序性」。為了解決 high-level 使用上的問題而更動了 low-level 處的內容。這樣是倒因為果,並不是一個好的作法。

從 top-down 的角度來說,責任歸屬並不應該那麼快交給 Collection 物件來處理。而是對順序性有需求的使用方來做。在取得所有物件之後,使用者再自己做排序就好。這應該是修改起來最快速也能夠使影響到的程式碼盡量少的情況。

從 bottom-up 的角度來說,我覺得原本在構造 associative container 時,就應該要意識到是以記憶體位置來作為順序,而使用者或許會有 sequential access 的需求,直接根據 unique_ptr 底下包裝的物件來做比較來使得順序是固定的。如果試問,使用者需要多於一種的順序性怎麼辦?或許這時可能要換個角度思考,為什麼對單一物件有多重屬性,是不是應該區分成兩種物件。

最後附上剛剛提到 overloading 的 code snippet

Author: eopXD

Hi 我是 eop ,希望人生過得有趣有挑戰XD

Leave a Reply