比起iterator更偏愛const_iterator
STL中的const_iterator等效於常量指針(pointers-to-const),它們指向的值是不能被修改的。標準實踐建議我們儘可能地使用const,同樣,如果迭代器指向的對象不需要修改,那麼我們應該使用const_iterator。
上面的說法在C++98和C++11中都是正確的,但是在C++98中,const_iterator只有部分支持,創建它們不容易,而且使用它也受限制。例如,你想要在std::vector<int>
中查找第一個1983的位置(那一年“C with Classes”編程語言更名爲“C++”),然後在那個位置插入數值1998(那一年C++第一個ISO標準被採用)。如果容器中不存在1983這個值,就在容器的末尾插入。使用C++98的迭代器,這很容易:
std::vector<int> values;
...
std::vector<int>::iterator it =
std::find(values.begin(), values.end(), 1983);
values.insert(it, 1998);
但是在這裏使用iterator不是很好,因爲代碼從來不會改變迭代器指向的對象。使用const_iterator更貼切於這裏的代碼,但是C++98會出現點問題。這裏有個方法在概念上是可靠的,不過仍然不正確:
typedef std::vector<int>::iterator IterT;
typedef std::vector<int>::const_iterator ConstIterT;
`
std::vector<int> values;
...
ConstIterT ci =
std::find(static_cast<ConstIterT>(values.begin()), // cast
static_cast<ConstIterT>(values.end()), // cast
1983);
`
values.insert(static_cast<IterT>(ci), 1998); // 可能編譯不通過,詳情看下面
當然,typedef不是必需的,不過它可以讓我們寫類型轉換時容易點(你可能會問爲什麼不遵循條款9的別名聲明,原因是這個例子展示的是C++98的代碼,而別名聲明是C++11的新特性)。
在函數std::find中出現了類型轉換,這是因爲values不是non-const容器,並且在C++98中從non-const容器中獲取const_iterator沒有簡單的辦法。在嚴格意義上,類型轉換不是必需的,因爲你可以從另外的途徑獲取const_iterator(例如,把values綁在一個常量引用(reference-to-const)的變量上,然後在代碼中用這個變量代替values),但是無論你使用哪種方法,從一個non-const容器獲取指向元素的const_iterator總會出現點意外。
如果你拿到了const_iterator,問題更多,因爲C++98容器的插入(和刪除)位置都是用iterator表示,const_iterator是不被接受的,這就爲什麼在上面的代碼中我把const_iterator轉換爲iterator:把const_iterator傳遞給insert是不能通過編譯的。
實話說,我給出的代碼還是無法通過編譯,因爲就算是用static_cast,關於const_iterator轉換到iterator也沒有輕便的慣例(意思就是不行?)就算是神器reinterpret_cast也不行哦(這不只是C++98的限制,在C++11中也不能簡單的把const_iterator轉換成iterator)。現在,我覺得我的觀點已經很清晰了:在C++98中const_iterator有那麼多問題,真的是不值得用。最後,開發者不應儘可能的使用const,而是在它實用(practical)時使用,然後在C++98中,const_iterator一點都不實用。
在C++11中一切都改變了。現在const_iterator即容易獲取又容易使用。就算是non-const容器,容器的成員函數cbegin和cend返回const_iterator,然後STL那些用iterator表示位置的成員函數(例如insert和earse)實際上都使用了const_iterator。把C++98舊代碼中iterator修改爲const_iterator在C++11中是再平常不過了:
std::vector<int> values;
...
auto it =
std::find(values.cbegin(), values.cend(), 1983); // 使用cbegin和cend
values.insert(it,1998);
現在使用const_iterator的代碼就很實用了!
C++11提供的獲取const_iterator的方法在你想要編寫最大限度通用庫代碼時略顯不足。這些代碼爲容器或者類似容器的數據結構提供非成員函數的begin和end(加上cbegin,cend,rbegin等)代碼。例如,就像內置的數組,它就是使用第三方庫提供的自由函數的接口。因此通用庫代碼就是使用非成員函數來實現已知的成員函數的功能。
例如,我們有個findAndInsert模板:
template <typename C, typename V>
void findAndInsert(C& container, // 在container中,
const V& targetVal, // 找到targetVal的第一次出現位置,
const V& insertVal) // 然後把insertVal插入那個位置
{
using std::cbegin;
using std::cend;
auto it = std::find(cbegin(container), // 非成員函數的cbegin
cend(container), //非成員函數的cend
targetVal);
container.insert(it, insertVal);
};
上面的代碼在C++14能運行得很好,但C++11不可以。在標準化期間疏忽了,C++11只加了非成員函數版本的begin和end,而沒有加入cbegin,cend,rbegin,rend,crbegin和crend。C++14修正了這問題。
如果你用的是C++11,但想寫出上面效果的代碼,卻沒有庫來提供一個非成員的cbegin模板,那麼你可以自己寫一個。例如,這裏有一個非成員cbegin的實現:
template <class C>
auto cbegin(const C& container)->decltype(std::begin(container))
{
return std::begin(container);
}
你是不是很驚訝這個非成員cbegin沒有調用成員cbegin 呢?其實我也是。不過這是有邏輯的。cbegin模板接受所有行爲像容器的參數C,然後用常量引用(reference-to-const)獲得這個參數container,如果C是傳統的容器類型(例如std::vector<int>
),container將會常量引用那個容器C(類型變成const std::vector<int>&
)。一個常量容器使用非成員函數begin(C++11提供)會返回一個const_iterator,這個類型就是模板返回的類型。你可以把這個由begin支持的cbegin直接用於容器。
這個模板也可以用於內置數組。這樣的話,container的就是一個常量數組引用。C++11爲數組提供了一個特別的begin,它返回指針數組首元素的指針。常量數組的元素是常量,所以begin返回的首元素指針是常量指針,常量指針,在實際上,也就是數組的常量迭代器。(數組的模板推斷請看條款1).
現在回到重點,這條款的觀點是鼓勵你儘量使用const_iterator。基本動機是——當const有意義時就使用——在C++11,而不是不實用的C++98。在C++11,它非常實用,C++14還解決了C++11留下的未完成的工作。
總結
需要記住的2點:
- 比起iterator更偏愛const_iterator。
- 在最大限度通用代碼中,比起成員函數,更偏向使用非成員版本的begin,end,rbegin等。