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_iterator;insert/erase 改為接受 const_iterator |
| 泛型程式碼 | 最大化泛型的程式碼應使用 non-member 版本的 begin、end、rbegin 等(支援內建陣列與只有 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 也不行 |
即使在 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 做了兩件事,讓上面的程式碼變得直截了當:
- 成員函式
cbegin/cend一律回傳const_iterator,即使容器本身是 non-const。 - STL 中以 iterator 指定位置的成員函式(如
insert、erase)改為接受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++11 只加入了 non-member begin/end,漏掉了 cbegin、cend、rbegin、rend、crbegin、crend。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_cast、reinterpret_cast 都不行(C++11/14 亦然) |
| 「寫最大化泛型的模板程式碼,begin/end 怎麼呼叫?」 | 用 non-member begin(container)/cbegin(container)(配合 using std::cbegin; 的兩段式慣用法),因內建陣列與部分第三方庫沒有成員版本 |
| 「C++11 有 non-member cbegin 嗎?」 | 沒有(標準化疏漏),只有 non-member begin/end;C++14 補齊 cbegin/cend/rbegin/rend/crbegin/crend |
| 「自製 non-member cbegin 該呼叫什麼?」 | 對 reference-to-const 參數呼叫 std::begin(而非成員 cbegin),const 容器的 begin 自然回傳 const_iterator,且相容於沒有成員 cbegin 的容器與內建陣列 |
Related Notes
- 04-Moving-To-Modern-Cpp/03-Alias-Declarations — 本 Item 的 C++98 範例只能用
typedef;C++11 起應改用 alias declaration(Item 9) - 04-Moving-To-Modern-Cpp/08-Noexcept — 同章下一個 Item:介面設計上另一種「盡可能標註」的原則
- 03-Auto/01-Prefer-Auto-To-Explicit-Types —
auto it = values.cbegin()省去冗長 iterator 型別,是 const_iterator 實用化的搭檔(Item 5) - 02-Deducing-Types/01-Template-Type-Deduction — non-member
begin對內建陣列的特化,依賴模板以 reference 參數推導陣列型別的機制(Item 1)