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 選擇 () 並文件化