“深入浅出”系列之C++:(16)区别()和{}创建对象

C++11 对象初始化的语法有点让人眼花缭乱。一般来说,初始化值要用圆括号 () 或者花括号 {} 括起来,或者放到等号 = 的右边:

int x(0);               //使用圆括号初始化
int y = 0;              //使用"="初始化
int z{ 0 };             //使用花括号初始化

这里的 = 运算符可能会让人产生误解,认为这里发生了赋值运算,然而实际并没有。对于像 int 这样的内置类型,没有讨论的意义,但是对于用户定义的类型而言,区别赋值运算符和初始化就非常重要了,因为它们涉及不同的函数调用:

Widget w1;              //调用默认构造函数
Widget w2 = w1;         //不是赋值运算,调用拷贝构造函数
w1 = w2;                //是赋值运算,调用拷贝赋值运算符(copy operator=)

C++11使用统一初始化(uniform initialization)来整合这些混乱且不适于所有情景的初始化语法,所谓统一初始化是指在任何涉及初始化的地方都使用单一的初始化语法。

std::vector<int> v{ 1, 3, 5 };  //v初始内容为1,3,5

class Widget{
    …

private:
    int x{ 0 };                 //没问题,x初始值为0
    int y = 0;                  //也可以
    int z(0);                   //错误!
}

  另一方面,不可拷贝的对象(例如std::atomic)可以使用花括号初始化或者圆括号初始化,但是不能使用"="初始化:

std::atomic<int> ai1{ 0 };      //没问题
std::atomic<int> ai2(0);        //没问题
std::atomic<int> ai3 = 0;       //错误!

  因此我们很容易理解为什么 {} 初始化又叫统一初始化,在 C++ 中这三种方式都被看做是初始化表达式,但是只有花括号任何地方都能被使用。括号表达式还有一个少见的特性,即它不允许内置类型间隐式的变窄转换(narrowing conversion)。如果一个使用了括号初始化的表达式的值,不能保证由被初始化的对象的类型来表示,代码就不会通过编译:

double x, y, z;
int sum1{ x + y + z };          //错误!double的和可能不能表示为int

  使用圆括号和"="的初始化不检查是否转换为变窄转换,因为由于历史遗留问题它们必须要兼容老旧代码:

int sum2(x + y +z);             //可以(表达式的值被截为int)
int sum3 = x + y + z;           //同上

  C++ 规定任何可以被解析为一个声明的东西必须被解析为声明。这个规则的副作用是让很多程序员备受折磨:他们可能想创建一个使用默认构造函数构造的对象,却不小心变成了函数声明。问题的根源是如果你调用带参构造函数,你可以这样做:

Widget w1(10);                  //使用实参10调用Widget的一个构造函数

  但是如果你尝试使用相似的语法调用Widget无参构造函数,它就会变成函数声明:

Widget w2();                    //最令人头疼的解析!声明一个函数w2,返回Widget

  由于函数声明中形参列表不能带花括号,所以使用花括号初始化表明你想调用默认构造函数构造对象就没有问题:

Widget w3{};                    //调用没有参数的构造函数构造对象

  {} 它的语法能用于各种不同的上下文,它防止了隐式的变窄转换,而且对于C++最令人头疼的解析也天生免疫。当然 {} 也有缺点,当涉及到 std::initializer_list 时,会使得括号初始化、std::initializer_list 和构造函数参与重载决议时本来就不清不楚的暧昧关系进一步混乱。
  在构造函数调用中,只要不包含 std::initializer_list 形参,那么花括号初始化和圆括号初始化都会产生一样的结果:

class Widget { 
public:  
    Widget(int i, bool b);      //构造函数未声明
    Widget(int i, double d);    //std::initializer_list这个形参 
    …
};
Widget w1(10, true);            //调用第一个构造函数
Widget w2{10, true};            //也调用第一个构造函数
Widget w3(10, 5.0);             //调用第二个构造函数
Widget w4{10, 5.0};             //也调用第二个构造函数

  然而,如果有一个或者多个构造函数的声明包含一个 std::initializer_list 形参,那么使用括号初始化语法的调用更倾向于选择带 std::initializer_list 的那个构造函数。如果编译器遇到一个括号初始化并且有一个带 std::initializer_list 的构造函数,那么它一定会选择该构造函数。如果上面的 Widget 类有一个 std::initializer_list<long double> 作为参数的构造函数,就像这样:

class Widget { 
public:  
    Widget(int i, bool b);      //同上
    Widget(int i, double d);    //同上
    Widget(std::initializer_list<long double> il);      //新添加的
    …
}; 

  w2 和 w4 将会使用新添加的构造函数,即使另一个非 std::initializer_list 构造函数和实参更匹配:

Widget w1(10, true);    //使用圆括号初始化,同之前一样
                        //调用第一个构造函数

Widget w2{10, true};    //使用花括号初始化,但是现在
                        //调用带std::initializer_list的构造函数
                        //(10 和 true 转化为long double)

Widget w3(10, 5.0);     //使用圆括号初始化,同之前一样
                        //调用第二个构造函数 

Widget w4{10, 5.0};     //使用花括号初始化,但是现在
                        //调用带std::initializer_list的构造函数
                        //(10 和 5.0 转化为long double)

  甚至普通构造函数和移动构造函数都会被带 std::initializer_list 的构造函数劫持:

class Widget { 
public:  
    Widget(int i, bool b);                              //同之前一样
    Widget(int i, double d);                            //同之前一样
    Widget(std::initializer_list<long double> il);      //同之前一样
    operator float() const;                             //转换为float
    …
};

Widget w5(w4);                  //使用圆括号,调用拷贝构造函数

Widget w6{w4};                  //使用花括号,调用std::initializer_list构造
                                //函数(w4转换为float,float转换为double)

Widget w7(std::move(w4));       //使用圆括号,调用移动构造函数

Widget w8{std::move(w4)};       //使用花括号,调用std::initializer_list构造
                                //函数(与w6相同原因)

  编译器一遇到括号初始化就选择带 std::initializer_list 的构造函数的决心是如此强烈,以至于就算带 std::initializer_list 的构造函数不能被调用,它也会硬选。

class Widget { 
public: 
    Widget(int i, bool b);                      //同之前一样
    Widget(int i, double d);                    //同之前一样
    Widget(std::initializer_list<bool> il);     //现在元素类型为bool
    …                                           //没有隐式转换函数
};

Widget w{10, 5.0};              //错误!要求变窄转换

  这里,编译器会直接忽略前面两个构造函数(其中第二个构造函数是所有实参类型的最佳匹配),然后尝试调用 std::initializer_list<bool> 构造函数。调用这个函数将会把 int(10) 和 double(5.0) 转换为 bool ,由于会产生变窄转换(bool不能准确表示其中任何一个值),括号初始化拒绝变窄转换,所以这个调用无效,代码无法通过编译。
  只有当没办法把括号初始化中实参的类型转化为 std::initializer_list 时,编译器才会回到正常的函数决议流程中。比如我们在构造函数中用 std::initializer_list<std::string> 代替 std::initializer_list<bool>,这时非 std::initializer_list 构造函数将再次成为函数决议的候选者,因为没有办法把 int 和 bool 转换为 std::string:

class Widget { 
public:  
    Widget(int i, bool b);                              //同之前一样
    Widget(int i, double d);                            //同之前一样
    //现在std::initializer_list元素类型为std::string
    Widget(std::initializer_list<std::string> il);
    …                                                   //没有隐式转换函数
};

Widget w1(10, true);     // 使用圆括号初始化,调用第一个构造函数
Widget w2{10, true};     // 使用花括号初始化,现在调用第一个构造函数
Widget w3(10, 5.0);      // 使用圆括号初始化,调用第二个构造函数
Widget w4{10, 5.0};      // 使用花括号初始化,现在调用第二个构造函数

  这里还有一个有趣的边缘情况。假如你使用的花括号初始化是空集,并且你欲构建的对象有默认构造函数,也有 std::initializer_list 构造函数。你的空的花括号意味着什么?如果它们意味着没有实参,就该使用默认构造函数,但如果它意味着一个空的 std::initializer_list,就该调用 std::initializer_list 构造函数。最终会调用默认构造函数。空的花括号意味着没有实参,不是一个空的 std::initializer_list

class Widget { 
public:  
    Widget();                                   //默认构造函数
    Widget(std::initializer_list<int> il);      //std::initializer_list构造函数

    …                                           //没有隐式转换函数
};

Widget w1;                      //调用默认构造函数
Widget w2{};                    //也调用默认构造函数
Widget w3();                    //最令人头疼的解析!声明一个函数

  如果你想用空 std::initializer 来调用 std::initializer_list 构造函数,你就得创建一个空花括号作为函数实参——把空花括号放在圆括号或者另一个花括号内来界定你想传递的东西。

Widget w4({});                  //使用空花括号列表调用std::initializer_list构造函数
Widget w5{{}};                  //同上

  std::vector 作为受众之一会直接受到影响。std::vector 有一个非 std::initializer_list 构造函数允许你去指定容器的初始大小,以及使用一个值填满你的容器。但它也有一个 std::initializer_list 构造函数允许你使用花括号里面的值初始化容器。如果你创建一个数值类型的 std::vector(比如 std::vector<int>),然后你传递两个实参,把这两个实参放到圆括号和放到花括号中有天壤之别:

std::vector<int> v1(10, 20);    //使用非std::initializer_list构造函数
                                //创建一个包含10个元素的std::vector,
                                //所有的元素的值都是20
std::vector<int> v2{10, 20};    //使用std::initializer_list构造函数
                                //创建包含两个元素的std::vector,
                                //元素的值为10和20

从以上讨论中我们得出两个重要结论。第一,作为一个类库作者,你需要意识到如果一堆重载的构造函数中有一个或者多个含有std::initializer_list 形参,用户代码如果使用了括号初始化,可能只会看到你 std::initializer_list 版本的重载的构造函数。因此,你最好把你的构造函数设计为不管用户是使用圆括号还是使用花括号进行初始化都不会有什么影响。换句话说,了解了 std::vector 设计缺点后,你以后设计类的时候应该避免诸如此类的问题。
另一方面如果一个类没有 std::initializer_list 构造函数,然后你添加一个,用户代码中如果使用括号初始化,可能会发现过去被决议为非 std::initializer_list 构造函数而现在被决议为新的函数。当然,这种事情也可能发生在你添加一个函数到那堆重载函数的时候:过去被决议为旧的重载函数而现在调用了新的函数。std::initializer_list 重载不会和其他重载函数比较,它直接盖过了其它重载函数,其它重载函数几乎不会被考虑。所以如果你要加入 std::initializer_list 构造函数,请三思而后行。
第二,必须在花括号和圆括号之间选择一个来创建对象。大多数开发者都使用其中一种作为默认情况,只有当他们不能使用这种的时候才会考虑另一种。默认使用花括号初始化的开发者主要被适用面广、禁止变窄转换、免疫 C++ 最令人头疼的解析这些优点所吸引。这些开发者知道在一些情况下(比如给定一个容器大小和一个初始值创建 std::vector)要使用圆括号。默认使用圆括号初始化的开发者主要被 C++98 语法一致性、避免 std::initializer_list 自动类型推导、避免不会不经意间调用 std::initializer_list 构造函数这些优点所吸引。这些开发者也承认有时候只能使用花括号(比如创建一个包含着特定值的容器)。

  • 花括号初始化是最广泛使用的初始化语法,它防止变窄转换,并且对于 C++ 最令人头疼的解析有天生的免疫性。

  • 在构造函数重载决议中,编译器会尽最大努力将括号初始化与 std::initializer_list 参数匹配,即便其他构造函数看起来是更好的选择。

  • 对于数值类型的 std::vector 来说使用花括号初始化和圆括号初始化会造成巨大的不同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值