每周技巧 #88:初始化:=、() 和 {}

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #88: Initialization: =, (), and {}

原文最初作为 TotW #88 发布于 2015 年 1 月 27 日。

作者:Titus Winters,代表 Google C++ Style Arbiters

C++11 提供了一种被称为“统一初始化语法”的新语法,本意是统一各种初始化风格,避免最令人烦恼的解析,并避免窄化转换。这个新机制意味着我们现在有了又一种初始化语法,而且它有自己的权衡。

C++11 花括号初始化

一些统一初始化语法的支持者会建议我们对所有类型的初始化都使用 {} 和直接初始化(不使用 =,尽管大多数情况下两种形式会调用同一个构造函数):

1
2
3
int x{2};
std::string foo{"Hello World"};
std::vector<int> v{1, 2, 3};

对比(例如):

1
2
3
int x = 2;
std::string foo = "Hello World";
std::vector<int> v = {1, 2, 3};

这种做法有两个缺点。第一,“统一”这个说法有点勉强:仍然存在一些情况,让普通读者(不是编译器)不清楚到底调用了什么、如何调用。

1
2
std::vector<std::string> strings{2}; // 包含两个空字符串的 vector。
std::vector<int> ints{2};            // 只包含整数 2 的 vector。

第二:这种语法并不完全直观;没有其他常见语言使用类似的东西。语言当然可以引入新的、令人惊讶的语法,而且在某些情况下确实有技术原因需要它,尤其是在泛型代码中。重要问题是:为了利用这种变化,我们应该在多大程度上改变自己的习惯和语言理解?收益是否值得我们改变习惯或现有代码的成本?对统一初始化语法而言,我们总体上不认为收益大于缺点。

初始化最佳实践

相反,对于“我该如何初始化一个变量?”,我们推荐以下指导原则。你既可以在自己的代码中遵循,也可以在代码评审中引用:

  • 当你直接用预期的字面值初始化时,请使用赋值语法。例如 intfloatstd::string 值,std::shared_ptrstd::unique_ptr 等智能指针,容器(std::vectorstd::map 等),执行 struct 初始化,或进行拷贝构造时。

    1
    2
    3
    4
    5
    6
    
    int x = 2;
    std::string foo = "Hello World";
    std::vector<int> v = {1, 2, 3};
    std::unique_ptr<Matrix> matrix = NewMatrix(rows, cols);
    MyStruct x = {true, 5.0};
    MyProto copied_proto = original_proto;
    

    而不是:

    1
    2
    3
    4
    5
    6
    7
    
    // 糟糕代码
    int x{2};
    std::string foo{"Hello World"};
    std::vector<int> v{1, 2, 3};
    std::unique_ptr<Matrix> matrix{NewMatrix(rows, cols)};
    MyStruct x{true, 5.0};
    MyProto copied_proto{original_proto};
    
  • 当初始化正在执行某些主动逻辑,而不是简单地把值组合在一起时,请使用传统构造函数语法(圆括号)。

    1
    2
    
    Frobber frobber(size, &bazzer_to_duplicate);
    std::vector<double> fifty_pies(50, 3.14);
    

    对比:

    1
    2
    3
    4
    5
    6
    7
    
    // 糟糕代码
    
    // 可能调用 initializer_list 构造函数,也可能调用双参数构造函数。
    Frobber frobber{size, &bazzer_to_duplicate};
    
    // 创建一个包含两个 double 的 vector。
    std::vector<double> fifty_pies{50, 3.14};
    
  • 只有当上面的选项无法编译时,才使用不带 ={} 初始化:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    class Foo {
     public:
      Foo(int a, int b, int c) : array_{a, b, c} {}
    
     private:
      int array_[5];
      // 因为构造函数标记为 explicit,
      // 且类型不可拷贝,所以这里需要 {}。
      EventManager em{EventManager::Options()};
    };
    
  • 绝不要混用 {} 和 auto。 例如,不要这样做:

    1
    2
    3
    
    // 糟糕代码
    auto x{1};
    auto y = {2}; // 这是 std::initializer_list<int>!
    

    (对语言律师来说:可用时优先使用拷贝初始化而不是直接初始化;不得不使用直接初始化时,优先使用圆括号而不是花括号。)

对这个问题最好的整体描述也许是 Herb Sutter 的 GotW 文章。虽然他展示的例子包括用花括号直接初始化 int,但他的最终建议大体上与这里的建议兼容,只有一个注意点:当 Herb 说“你偏好只看到等号的地方”时,我们明确偏好看到的就是等号。结合对多参数构造函数更一致地使用 explicit(见 技巧 #142),这在可读性、显式性和正确性之间提供了一种平衡。

结论

统一初始化语法的权衡通常不值得:我们的编译器已经会对最令人烦恼的解析发出警告(你可以使用花括号初始化,或添加括号来解决问题),而避免窄化转换带来的安全性,不值得用花括号初始化造成的可读性损失(最终我们需要另一种解决窄化转换的方案)。Style Arbiters 认为这个问题还没有关键到需要制定正式规则,尤其是因为某些情况(特别是泛型代码)中,花括号初始化可能是合理的。

每周技巧 #86:用类枚举

上一节

每周技巧 #90:退役的标志

下一节