實現你自己的迭代器
使用 std::iterator
在 C++17 之前,實現自定義的迭代器被推薦採用從 std::iterator 派生的方式。
std::iterator 的基本定義
Std::iterator 具有這樣的定義:
template<
class Category,
class T,
class Distance = std::ptrdiff_t,
class Pointer = T*,
class Reference = T&
> struct iterator;
其中,T 是你的容器類型別,無需多提。而 Category 是必須首先指定的所謂的 迭代器標籤
,參考 這裡 。Category 主要可以是:
- input_iterator_tag:輸入迭代器
- output_iterator_tag:輸出迭代器
- forward_iterator_tag:前向迭代器
- bidirectional_iterator_tag:雙向迭代器
- random_access_iterator_tag:隨機訪問迭代器
- contiguous_iterator_tag:連續迭代器
這些標籤看起來似乎相當莫名其妙,彷彿我知道它們的用意,但實際上卻又難以明白,難以挑選。
迭代器標籤
下面粗略地對它們及其關聯實體進行特性上的介紹,以幫助你理解。
這些 tags 實際上繫結關聯著一些同名實體類如 input_iterator 等等,通過模板特化技術分別實現專有的 distance() 和 advance() ,以達到特定的迭代優化效果。
input_iterator_tag
input_iterator_tag
可以包裝函式的輸出——以用作它人的輸入流。所以它是僅可遞增的(只能 +1),你不能對它 +n,只能通過迴圈 n 次遞增來模擬相應的效果。input_iterator 無法遞減(-1),因為輸入流沒有這樣的特性。它的迭代器值(*it
)是隻讀的,你不能對其置值。
但 output_iterator_tag,forward_iterator_tag 的迭代器值是可讀寫的。可讀寫的迭代器值是指:
std::list<int> l{1,2,3};
auto it = l.begin();
++it;
(*it) = 5; // <- set value back into the container pointed by iterator
input_iterator
將容器呈現為一個輸入流,你可以通過 input_iterator 接收輸入資料流。
output_iterator_tag
output_iterator_tag
很少被使用者直接使用,它通常和 back_insert_iterator/ front_insert_iterator/ insert_iterator 以及 ostream_iterator 等配合使用。
output_iterator
沒有 ++/-- 能力。你可以向 output_iterator
指向的容器中寫入/置入新值,僅此而已。
如果你有輸出流樣式的呈現需求,可以選擇它。
forward_iterator_tag
forward_iterator_tag
表示前向迭代器,所以只能增量,不能回退,它繼承 input_iterator_tag
的一切基本能力,但又有所增強,例如允許設定值。
從能力上說,input_iterator
支援讀取/設定值,也支援遞增行走,不支援遞減行走(需要模擬,低效),+n 需要用迴圈模擬故而低效,但如果你的容器只有這樣的外露的需求,那麼 forward_iterator_tag
就是最佳選擇。
從理論上來說,支援 forward_iterator_tag
的迭代器必須至少實現 begin/end。
bidirectional_iterator_tag
bidirectional_iterator_tag
的關聯實體 bidirectional_iterator
是雙向可行走的,既可以 it++
也可以 it--
,例如 std::list。如同 forward_iterator_tag
一樣,bidirectional_iterator_tag
不能直接 +n (和 -n),所以 +n 需要一個特化的 advance 函式來迴圈 n 次,每次 +1(即通過迴圈 n 次遞增或遞減來模擬)。
從理論上來說,支援 bidirectional_iterator_tag
的迭代器必須同時實現 begin/end 以及 rbegin/rend。
random_access_iterator_tag
random_access_iterator_tag
表示的隨機訪問迭代器,random_access_iterator
支援讀取/設定值,支援遞增遞減,支援 +n/-n。
由於 random_access_iterator
支援高效的 +n/-n,這也意味著它允許高效的直接定位,這種迭代器的所屬容器,通常也順便支援 operator []
下標存取,如同 std::vector 那樣。
contiguous_iterator_tag
contiguous_iterator_tag
在 C++17 中開始引入,但是編譯器們的支援力度有問題,所以目前我們不能對其進行詳細介紹,對於實作來說不必考慮它的存在。
自定義迭代器的實現
一個定製迭代器需要選擇一個迭代器標籤,也就是選擇迭代器的支援能力集合。下面是一個示例:
namespace customized_iterators {
template<long FROM, long TO>
class Range {
public:
// member typedefs provided through inheriting from std::iterator
class iterator : public std::iterator<std::forward_iterator_tag, // iterator_category
long, // value_type
long, // difference_type
const long *, // pointer
const long & // reference
> {
long num = FROM;
public:
iterator(long _num = 0)
: num(_num) {}
iterator &operator++() {
num = TO >= FROM ? num + 1 : num - 1;
return *this;
}
iterator operator++(int) {
iterator ret_val = *this;
++(*this);
return ret_val;
}
bool operator==(iterator other) const { return num == other.num; }
bool operator!=(iterator other) const { return !(*this == other); }
long operator*() { return num; }
};
iterator begin() { return FROM; }
iterator end() { return TO >= FROM ? TO + 1 : TO - 1; }
};
void test_range() {
Range<5, 13> r;
for (auto v : r) std::cout << v << ',';
std::cout << '\n';
}
}
這個示例的原型來自於 cppreference 上 std::iterator 及其原作者,略有修改。
自增自減運算子過載
專門獨立一個小節,因為發現垃圾教程太多了。
自增自減的運算子過載分為字首字尾兩種形式,字首方式返回引用,字尾方式返回新副本:
struct X {
// 字首自增
X& operator++() {
// 實際上的自增在此進行
return *this; // 以引用返回新值
}
// 字尾自增
X operator++(int) {
X old = *this; // 複製舊值
operator++(); // 字首自增
return old; // 返回舊值
}
// 字首自減
X& operator--() {
// 實際上的自減在此進行
return *this; // 以引用返回新值
}
// 字尾自減
X operator--(int) {
X old = *this; // 複製舊值
operator--(); // 字首自減
return old; // 返回舊值
}
};
或者去檢視 cppreference 的 文件 以及 文件,別去看那些教程了,找不出兩個正確的。
正確的編碼是實現一個字首過載,然後基於它實現字尾過載:
struct incr {
int val{};
incr &operator++() {
val++;
return *this;
}
incr operator++(int d) {
incr ret_val = *this;
++(*this);
return ret_val;
}
};
如果有必要,你可能需要實現 operator=
或者 X(X const& o)
拷貝建構函式。但對於簡單平凡 struct
來說可以省略(如果你不能確定自動記憶體拷貝是否被提供,考慮檢視彙編程式碼,或者乾脆顯式實現 operator=
或者 X(X const& o)
拷貝建構函式)
C++17 起
但從 C++17 起 std::iterator 被棄用了。
如果你真的很關心流言飛語,可以去 這裡 看看有關的討論。
在多數情況下,你仍然可以使用 std::iterator 來簡化程式碼編寫,但這一特性以及早期的迭代器標籤、類別等等概念已經過時。
完全手寫迭代器
所以在從 C++17 開始的新時代,自定義迭代器原則上暫時只有手寫。
namespace customized_iterators {
namespace manually {
template<long FROM, long TO>
class Range {
public:
class iterator {
long num = FROM;
public:
iterator(long _num = 0)
: num(_num) {}
iterator &operator++() {
num = TO >= FROM ? num + 1 : num - 1;
return *this;
}
iterator operator++(int) {
iterator ret_val = *this;
++(*this);
return ret_val;
}
bool operator==(iterator other) const { return num == other.num; }
bool operator!=(iterator other) const { return !(*this == other); }
long operator*() { return num; }
// iterator traits
using difference_type = long;
using value_type = long;
using pointer = const long *;
using reference = const long &;
using iterator_category = std::forward_iterator_tag;
};
iterator begin() { return FROM; }
iterator end() { return TO >= FROM ? TO + 1 : TO - 1; }
};
} // namespace manually
void test_range() {
manually::Range<5, 13> r;
for (auto v : r) std::cout << v << ',';
std::cout << '\n';
}
}
示例中的 iterator traits 部分不是必需的,你完全可以不必支援它們。
需要照顧到的事情
完全手寫迭代器的注意事項包括:
- begin() 和 end()
迭代器嵌入類(不必被限定為嵌入),至少實現:
- 遞增運算子過載,以便行走
- 遞減運算子過載,如果是雙向行走(bidirectional_iterator_tag)或隨機行走(random_access_iterator_tag)
operator*
運演算法過載,以便迭代器求值operator!=
運算子過載,以便計算迭代範圍;必要時也可以顯式過載operator==
(預設時編譯器自動從!=
運算子上生成一個配套替代品)
如果你編碼對迭代範圍進行了支援,那麼就可以使用 for 範圍迴圈:
your_collection coll;
for(auto &v: coll) {
std::cout << v << '\n';
}
關於 for 範圍迴圈的展開式,可以檢視 這裡。
C++20 之後
在 C++20 之後,迭代器發生了巨大的變化。但由於它的工程實作還早的很,所以本文中暫且不予討論。
其它相關
除了 iterator 還有 const_iterator
為了程式碼規範和安全性,getter 通常一次提供兩個,可寫的和不可寫的:
struct incr {
int &val(){ return _val; }
int const &val() const { return _val; }
private:
int _val{};
}
同樣的道理,迭代器的 begin() 和 end() 也至少要提供 const 和 非 const 的兩種版本。一般來說你可以通過獨立實現來幫助提供多套版本:
struct XXX {
// ... struct leveled_iter_data {
// static leveled_iter_data begin(NodePtr root_) {...}
//. static leveled_iter_data end(NodePtr root_) {...}
// }
using iterator = leveled_iter_data;
using const_iterator = const iterator;
iterator begin() { return iterator::begin(this); }
const_iterator begin() const { return const_iterator::begin(this); }
iterator end() { return iterator::end(this); }
const_iterator end() const { return const_iterator::end(this); }
}
這是不費腦子的一種方式,讀寫安全性被約束在 XXX 之內:owner 當然能夠明白哪些應該可被暴露,哪些需要暫時約束暴露出來的能力。
除了 iterator 和 const_iterator 之外,rbegin/rend, cbegin/cend 等也可以考慮被實現。
注意事項:迭代器的使用
迭代器的使用一定要注意隨用隨取的準則。
void test_iter_invalidate() {
std::vector<int> vi{3, 7};
auto it = vi.begin();
it = vi.insert(it, 11);
vi.insert(it, 5000, 23);
vi.insert(it, 1, 31); // crach here!
std::cout << (*it) << '\n';
return;
}
在多數 OS 環境中,vi.insert(it, 5000, 23);
語句有極大概率導致 vector 不得不重新分配內部的陣列空間,因此該語句執行之後,it 所持有的內部指標就已經無意義了(it 仍指向舊的緩衝區的某個位置),所以下一行語句繼續使用 it 將會導致錯誤的指向與寫入。由於過時的緩衝區有很大的可能已經被排程處於缺頁狀態,所以這個錯誤往往會導致 SIGSEGV 致命異常。如果產生了 SIGSEGV 訊號,你可能是很幸運的,反而若是過時的緩衝區尚且有效,那麼這一語句能夠被執行且不報任何錯誤,那才是要命。
迭代器的搜尋並刪除
stdlib 的容器採用一種叫做 erase and remove 的慣用法來事實上刪除一個元素。以 std::list 為例,remove_if() 能夠從 list 中找到符合條件的元素,並將他們聚集(收集)起來移動到 list 的末尾,然後返回這組元素中的第一個元素的位置 iter,然而這些元素並未被從 list 中刪除,如果你需要去掉他們的話,你需要以 list.erase(iter, list.end()) 來明確地移除它們。
所以刪除元素是這樣的:
bool IsOdd(int i) { return i & 1; }
std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
v.erase(std::remove_if(v.begin(), v.end(), IsOdd), v.end());
std::list<int> l = { 1,100,2,3,10,1,11,-1,12 };
l.erase(l.remove_if(IsOdd), l.end());
由於 std::vector 不能像 std::list 那樣聚集元素到連結串列末尾,所以它沒有 remove_if() 成員函式,故而在它上面做 search & erase 需要 std::remove_if 的參與。而 std::list 可以直接使用成員函式 remove_if 來完成,程式碼也顯得稍微簡潔一些。
自 C++20 起,erase and remove_if 可以被簡化為 std::erase_if() 或 erase_if() 成員函式,例如 std::erase, std::erase_if (std::vector) 。
後記
這次的 About customizing your own STL-like iterator 貢獻了一些個人理解和最佳實踐的準則,但是還有點點意猶未盡。
下回考慮是不是介紹一個 tree_t 及其迭代器實現,或許能夠更有參考價值。
Refs
- 基於範圍的 for 迴圈 (C++11 起) - cppreference.com
- std::iterator - cppreference.com
- std::input_iterator_tag, std::output_iterator_tag, std::forward_iterator_tag, std::bidirectional_iterator_tag, std::random_access_iterator_tag, std::contiguous_iterator_tag - cppreference.com
- Increment/decrement operators - cppreference.com
- operator overloading - cppreference.com
- Traversing Trees with Iterators
- c++ - How to implement an STL-style iterator and avoid common pitfalls? - Stack Overflow
- c++ - Writing your own STL Container - Stack Overflow
STL & Generic Programming: Writing Your Own Iterators - Dr Dobb's
c++ - How to correctly implement custom iterators and const_iterators? - Stack Overflow