Item 7:建立物件時區分 () 與 {} (Distinguish between () and {} when creating objects)
Overview Table
| 面向 | {}(braced initialization) |
()(parentheses) |
= |
|---|---|---|---|
| 適用範圍 | 最廣:幾乎所有情境皆可用 | 不能用於類別內非靜態成員預設值 | 不能用於 uncopyable 物件(如 std::atomic) |
| narrowing conversion | 禁止(編譯錯誤) | 允許(靜默截斷) | 允許 |
| most vexing parse | 免疫(Widget w{}; 呼叫建構子) |
受害(Widget w(); 宣告函式) |
不受影響 |
std::initializer_list 建構子 |
強烈優先匹配,甚至劫持 copy/move | 正常 overload resolution | — |
| auto 推導 | 推導為 std::initializer_list(見 Item 2) |
推導為一般型別 | 推導為 std::initializer_list |
本 Item 核心:
{} 是「最通用」的初始化語法,但它與 std::initializer_list 建構子的糾纏會導致出人意料的行為——沒有絕對的贏家,選一種當預設並貫徹使用。為什麼 {} 被稱為 uniform initialization
C++98 的三種初始化語法各有死角,只有 {} 幾乎處處可用:
int x(0); // 小括號初始化
int y = 0; // 等號初始化
int z{ 0 }; // 大括號初始化(braced initialization)
class Widget {
private:
int x{ 0 }; // OK:成員預設值可用 {}
int y = 0; // OK:也可用 =
int z(0); // 錯誤!成員預設值不能用 ()
};
std::atomic<int> ai1{ 0 }; // OK
std::atomic<int> ai2(0); // OK
std::atomic<int> ai3 = 0; // 錯誤!uncopyable 物件不能用 =
std::vector<int> v{ 1, 3, 5 }; // C++98 無法直接表達的容器內容初始化
{} 的三大優點(trifecta of goodness):
| 優點 | 範例 |
|---|---|
| 適用範圍最廣 | 唯一三處(區域變數、成員預設值、atomic)皆可用的語法 |
| 禁止 narrowing conversion | int sum1{ x + y + z }; 若 x/y/z 為 double → 編譯錯誤;() 與 = 則靜默截斷 |
| 免疫 most vexing parse | Widget w2(); 宣告函式;Widget w3{}; 呼叫預設建構子 |
「uniform(處處可用)」是概念上的簡化:
auto 變數配上 {} 會推導成 std::initializer_list(而非直覺型別),這是 {} 實務上最大的例外之一——越愛用 auto,越要小心 {}(見 Item 2)。std::initializer_list 建構子的強力劫持
只要類別沒有 std::initializer_list 建構子,() 與 {} 意義相同。一旦有了,{} 呼叫會極盡所能地匹配 std::initializer_list 版本,即使其他建構子的匹配明顯更好:
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
Widgetinitializer_list<long double> il; // 新增後改變一切
operator float() const; // 可轉成 float
};
Widget w1(10, true); // ():呼叫第一個建構子
Widget w2{10, true}; // {}:呼叫 initializer_list 版本!
// (10 與 true 被轉成 long double)
Widget w4{10, 5.0}; // {}:同樣被劫持
Widget w5(w4); // ():呼叫 copy ctor
Widget w6{w4}; // {}:呼叫 initializer_list 版本!
// (w4 → float → long double)
Widget w8{std::move(w4)}; // {}:連 move ctor 都被劫持
overload resolution 的決策流程:
Widget w{args...};
│
▼
存在 std::initializer_list 建構子? ──否──▶ 正常 overload resolution
│是
▼
args 有任何辦法轉換成 list 元素型別? ──否──▶ fallback:正常 overload resolution
│是 (例:int/bool 無法轉 std::string)
▼
強制選用 initializer_list 建構子
│
▼
轉換是 narrowing? ──是──▶ 編譯錯誤!(不會回頭考慮更佳匹配的其他建構子)
│否
▼
呼叫 initializer_list 建構子
class Widget {
public:
Widget(int i, bool b); // 第二個是完全匹配
Widget(int i, double d);
Widgetinitializer_list<bool> il; // 元素型別為 bool
};
Widget w{10, 5.0}; // 錯誤!編譯器堅持用 initializer_list<bool>,
// 但 10 與 5.0 → bool 是 narrowing → 直接拒絕編譯
空大括號是例外:
Widget w2{}; 呼叫預設建構子,不是空的 std::initializer_list——空大括號的意義是「沒有引數」。若真要傳空 list,須寫 Widget w4({}); 或 Widget w5{{}};。(Widget w3(); 則又是 most vexing parse,宣告了函式!)std::vector 的教訓與模板作者的兩難
最著名的受害者是 std::vector<numeric type>——兩個引數、括號不同、結果天差地遠:
std::vector<int> v1(10, 20); // 非 initializer_list 建構子:
// 10 個元素,每個值為 20
std::vector<int> v2{10, 20}; // initializer_list 建構子:
// 2 個元素,值為 10 與 20
兩個教訓:
| 角色 | 教訓 |
|---|---|
| 類別作者 | 新增 std::initializer_list 建構子會**遮蔽(overshadow)**既有 overload,使 {} 呼叫可能只看得到它——設計建構子時應讓 ()/{} 不影響呼叫結果;std::vector 介面如今被視為設計錯誤 |
| 類別使用者 | 在 () 與 {} 中擇一為預設、貫徹使用,僅在必要時用另一種;兩派皆合理,無共識何者較佳 |
| 模板作者 | 模板內部無從得知該用 () 還是 {}——只有呼叫端知道意圖 |
template<typename T, typename... Ts>
void doSomeWork(Ts&&... params)
{
T localObjectforward<Ts>(params)...); // 用 (:v 有 10 個元素
T localObject{std::forward<Ts>(params)...}; // 用 {}:v 有 2 個元素
}
doSomeWork<std::vector<int>>(10, 20); // 哪個對?作者無法知道!
標準函式庫的 std::make_unique / std::make_shared 面臨相同問題,解法是內部一律使用 (),並將此決定寫進介面文件(見 Item 21)。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
Widget w(); 是什麼? |
most vexing parse:宣告回傳 Widget 的函式,非物件;改用 Widget w{}; |
int x{ d };(d 為 double) |
編譯錯誤:{} 禁止 narrowing conversion;int x(d); 則靜默截斷 |
類別有 initializer_list 建構子,{} 呼叫選誰? |
極力優先 initializer_list 版本,即使其他建構子匹配更佳;連 copy/move 都可能被劫持 |
| 匹配到 initializer_list 但需要 narrowing | 編譯錯誤,不會 fallback 到其他建構子 |
| 引數完全無法轉成 list 元素型別(如 int → std::string) | 才會 fallback 至正常 overload resolution |
Widget w{};(類別同時有預設與 initializer_list 建構子) |
呼叫預設建構子;空 list 要寫 w({}) 或 w{{}} |
std::vector<int> v(10, 20) vs v{10, 20} |
() → 10 個 20;{} → 兩個元素 10、20 |
auto x{ 1 }; 推導型別 |
std::initializer_list<int>(Item 2 的陷阱) |
模板內建立 T 物件該用哪種括號? |
無法通用決定;make_unique/make_shared 選擇 () 並文件化 |
Related Notes
- 04-Moving-To-Modern-Cpp/02-Nullptr — 同章下一則:另一個「舊語法陷阱 → 新慣用法」的主題
- 04-Moving-To-Modern-Cpp/11-Special-Member-Function-Generation — copy/move 建構子的自動生成規則,理解
{}劫持 copy/move 的前提 - 02-Deducing-Types/02-Auto-Type-Deduction —
auto+{}推導為std::initializer_list的特殊規則(Item 2) - 05-Smart-Pointers/04-Make-Unique-And-Make-Shared — make 函式內部固定用
()轉發引數,無法完美轉發 braced initializer(Item 21) - 09-Tweaks/02-Emplacement — emplace 系列同樣以
()直接建構,與 insert +{}行為差異(Item 42) - 04-Moving-To-Modern-Cpp/Practice-Moving-To-Modern-Cpp — 本章練習題