Effective Modern C++ 條款13 比起iterator更偏愛const_iterator

比起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容器,容器的成員函數cbegincend返回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的方法在你想要編寫最大限度通用庫代碼時略顯不足。這些代碼爲容器或者類似容器的數據結構提供非成員函數的beginend(加上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只加了非成員函數版本的beginend,而沒有加入cbegincendrbeginrendcrbegincrend。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
  • 在最大限度通用代碼中,比起成員函數,更偏向使用非成員版本的beginendrbegin等。
發佈了18 篇原創文章 · 獲贊 43 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章