文章

条款7:区别使用()和{}创建对象

花括号初始化更通用,可防止窄化转换,避免最令人头疼的解析;但重载时易优先匹配 std::initializer_list 构造函数,需谨慎使用。

条款7:区别使用()和{}创建对象

条款7:区别使用()和{}创建对象

C++ 中的三种初始化方式

方式示例说明
圆括号 ()int x(0);传统构造语法
等号 =int x = 0;赋值初始化
花括号 {}int x{0};C++11 引入的统一初始化方式,称作 brace initialization

花括号初始化的优点

  1. 最统一的语法:可用于所有类型、所有上下文,包括成员变量初始化。

  2. 禁止变窄转换(narrowing conversion)

    1
    
    int x{2.5}; // 错误,防止精度丢失
    
  3. 规避 most vexing parse 问题:

    1
    2
    
    Widget w1();  // 解析成函数声明(最令人头疼的解析)
    Widget w2{};  // 正确调用默认构造函数
    
    • most vexing parse最令人头疼的解析)是 C++ 语法中的一个 经典陷阱,指的是:本来想写对象的定义,结果却被 C++ 编译器解析成了函数声明
    • 默认构造一个对象时,{} 是最安全的方式。
  4. 支持直接列表初始化容器

    1
    
    std::vector<int> v{1, 2, 3};
    

花括号初始化的陷阱

优先匹配 std::initializer_list 构造函数

如果存在如下构造函数:

1
2
3
4
5
6
class Widget {
public:
    Widget(int, bool);
    Widget(std::initializer_list<long double>);
};
Widget w{10, true}; // 调用 initializer_list 构造函数,而非更匹配的 Widget(int, bool)

即使其他构造函数匹配更好,编译器也更偏向选择 std::initializer_list 版本。

会因变窄转换而导致编译失败

1
2
Widget(std::initializer_list<bool>);
Widget w{10, 5.0}; // 错误,因 bool 无法安全表示 10 和 5.0

花括号 vs 圆括号 初始化 std::vector<int>

1
2
std::vector<int> v1(10, 20); // 10个元素,值为20
std::vector<int> v2{10, 20}; // 2个元素,值为10, 20

两者行为完全不同,这类情形尤需小心选择。

类库作者的建议

  • 如果提供了 std::initializer_list 构造函数,要确保不会意外劫持其他构造函数。
  • 明确设计类时,花括号或圆括号初始化都应该能产生一致行为。
  • 谨慎新增 std::initializer_list 构造函数,可能会破坏原有初始化逻辑。

模板中的挑战

在模板中,初始化方式影响结果:

1
2
3
4
5
template<typename T, typename... Args>
void doSomeWork(Args&&... args) {
    T obj(std::forward<Args>(args)...);  // 使用圆括号
    // 或者 T obj{std::forward<Args>(args)...}; // 使用花括号
}

调用时:

1
doSomeWork<std::vector<int>>(10, 20);
  • 圆括号:创建10个值为20的元素
  • 花括号:创建两个元素,值为10 和 20

标准库的 make_sharedmake_unique 统一使用圆括号避免歧义。

总结建议

  1. 默认使用花括号 {} 初始化,除非:
    • 需要调用非 initializer_list 构造函数
    • 可能引起构造函数选择混乱
  2. 避免使用 = 初始化,容易混淆赋值与初始化语义
  3. 设计类时要意识到 initializer_list 的优先级问题
  4. 模板中初始化方式对行为影响极大,慎选 () vs {}
本文由作者按照 CC BY 4.0 进行授权