Item 13:優先使用 const_iterator 而非 iterator (Prefer const_iterators to iterators)

Overview Table

主題 重點
核心原則 只要不需修改 iterator 所指的值,就應使用 const_iterator(「盡可能使用 const」的 iterator 版)
const_iterator 本質 STL 中 pointer-to-const 的對應物,指向不可修改的值
C++98 困境 從 non-const 容器取得 const_iterator 很麻煩;insert/erase 只接受 iterator,使 const_iterator 幾乎不實用
C++11 改善 成員函式 cbegin/cend 對 non-const 容器也回傳 const_iteratorinsert/erase 改為接受 const_iterator
泛型程式碼 最大化泛型的程式碼應使用 non-member 版本的 beginendrbegin 等(支援內建陣列與只有 free function 介面的第三方庫)
C++11 缺口 C++11 只有 non-member begin/end;non-member cbegin/cend/rbegin/rend/crbegin/crend 要到 C++14 才補齊
不可逆轉換 const_iterator 無法可攜地轉回 iterator(連 reinterpret_cast 都不行)

const_iterator 在 C++98 為何不實用

C++98 想在「找到 1983 後於該位置插入 1998」的程式碼中改用 const_iterator,會遇到兩大障礙:

// C++98:用 iterator 很簡單
std::vector<int>::iterator it =
  std::find(values.begin(), values.end(), 1983);
values.insert(it, 1998);              // 但這段程式碼從未修改元素,
                                      // 理論上應該用 const_iterator

// C++98:改用 const_iterator 卻困難重重
typedef std::vector<int>::iterator IterT;               // C++98 只能用
typedef std::vector<int>::const_iterator ConstIterT;    // typedef

ConstIterT ci =
  std::find(static_cast<ConstIterT>(values.begin()),    // 障礙 1:non-const 容器
            static_cast<ConstIterT>(values.end()),      // 拿不到 const_iterator,
            1983);                                      // 只能繞路轉型

values.insert(static_cast<IterT>(ci), 1998);            // 障礙 2:insert 只收
                                                        // iterator;此轉型
                                                        // 甚至可能無法編譯
障礙 說明
取得困難 non-const 容器的 begin()/end() 回傳 iterator,C++98 沒有簡單方法取得 const_iterator(得靠轉型或綁定 reference-to-const 等迂迴手段)
使用受限 C++98 的插入/刪除位置只能用 iterator 指定const_iterator 不被接受
無法轉回 const_iterator 不能可攜地轉換成 iterator——static_cast 不行,連 reinterpret_cast 也不行
const_iterator → iterator 的轉換禁令不是 C++98 限制

即使在 C++11/C++14,const_iterator 依然無法轉換成 iterator——兩者是不同型別,任何 cast 都不可攜。可攜地產生「指向相同位置的 iterator」的方法存在,但不明顯也不通用。結論:開發者實務上是「practical 才用 const」,而非「凡可能就用 const」——C++98 的 const_iterator 正是不 practical 的例子。

C++11:cbegin/cend 讓 const_iterator 變得實用

C++11 做了兩件事,讓上面的程式碼變得直截了當:

  1. 成員函式 cbegin/cend 一律回傳 const_iterator即使容器本身是 non-const
  2. STL 中以 iterator 指定位置的成員函式(如 inserterase)改為接受 const_iterator
std::vector<int> values;                       // non-const 容器
// ...
auto it = std::find(values.cbegin(),           // cbegin/cend 直接給出
                    values.cend(), 1983);      // const_iterator
values.insert(it, 1998);                       // insert 接受 const_iterator,
                                               // 直接編譯通過

搭配 auto(Item 5)連冗長的 iterator 型別名稱都省了——這才是 practical 的 const_iterator 程式碼。

const_iterator 支援演進
=======================
C++98:  取得困難 ──▶ insert/erase 不收 ──▶ 幾乎沒人用
C++11:  member cbegin/cend ✔   insert/erase 收 const_iterator ✔
        但 non-member 只有 begin/end(漏掉 cbegin 等)✘
C++14:  non-member cbegin/cend/rbegin/rend/crbegin/crend 補齊 ✔

最大化泛型程式碼:優先使用 non-member begin/end

有些「容器狀」資料結構(內建陣列、只提供 free function 介面的第三方庫)沒有成員函式 begin/end。最大化泛型的程式碼因此不應假設成員版本存在,而應呼叫 non-member 版本:

template<typename C, typename V>
void findAndInsert(C& container,               // 在 container 中找到
                   const V& targetVal,         // targetVal 第一次出現處,
                   const V& insertVal)         // 於該處插入 insertVal
{
  using std::cbegin;                           // 兩段式呼叫:讓 ADL 與
  using std::cend;                             // std:: 版本都可被選中

  auto it = std::find(cbegin(container),       // non-member cbegin
                      cend(container),         // non-member cend
                      targetVal);

  container.insert(it, insertVal);
}
這段程式碼在 C++14 可行,在 C++11 不行

標準化時的疏漏:C++11 只加入了 non-member begin/end漏掉了 cbegincendrbeginrendcrbegincrend。C++14 才補齊。C++11 環境下若函式庫也沒提供,可自行補上實作(見下)。

C++11 中自製 non-member cbegin 出乎意料地簡單——它呼叫的是 std::begin 而非成員 cbegin

template <class C>
auto cbegin(const C& container)->decltypebegin(container)
{
  return std::begin(container);   // 對 const 容器呼叫 begin
}                                 // 即得 const_iterator
資料流:為何呼叫 std::begin 就能得到 const_iterator

  任意容器 C ──綁定──▶ const C& container(參數是 reference-to-const)
                              │
                              ▼
                      std::begin(container)
                       │              │
        C 是一般容器    │              │  C 是內建陣列 T[N]
                       ▼              ▼
        呼叫 const 容器的           特化版回傳 const T*
        成員 begin()               (指向首元素)
                       │              │
                       └──────┬───────┘
                              ▼
                 const_iterator(pointer-to-const
                  就是陣列的 const_iterator)

這種寫法的優點:對只有成員 begin沒有成員 cbegin 的容器也能運作;對內建陣列同樣有效(C++11 為陣列特化的 non-member begin 回傳指向首元素的指標,const 陣列的元素為 const,故得 pointer-to-const)。陣列如何被模板特化處理,參見 Item 1 對「以 reference 參數接陣列」的型別推導討論。

Exam/Test Patterns

情境關鍵字 答案
「遍歷容器但不修改元素,該用什麼 iterator?」 const_iterator(透過 cbegin/cend 取得)
「non-const 容器如何取得 const_iterator?」 C++11 起用成員 cbegin()/cend();C++98 只能轉型或綁定 reference-to-const 等迂迴手段
insert/erase 能接受 const_iterator 嗎?」 C++11 起可以(位置參數即為 const_iterator);C++98 只收 iterator
「const_iterator 能 cast 成 iterator 嗎?」 不能——沒有可攜的轉換,static_castreinterpret_cast 都不行(C++11/14 亦然)
「寫最大化泛型的模板程式碼,begin/end 怎麼呼叫?」 non-member begin(container)/cbegin(container)(配合 using std::cbegin; 的兩段式慣用法),因內建陣列與部分第三方庫沒有成員版本
「C++11 有 non-member cbegin 嗎?」 沒有(標準化疏漏),只有 non-member begin/endC++14 補齊 cbegin/cend/rbegin/rend/crbegin/crend
「自製 non-member cbegin 該呼叫什麼?」 reference-to-const 參數呼叫 std::begin(而非成員 cbegin),const 容器的 begin 自然回傳 const_iterator,且相容於沒有成員 cbegin 的容器與內建陣列