Item7 Distinguish between () and {} when creating objects

   在引入C++11后变量的初始化方式多种多样,对于每种初始化的方式的区别和联系是一个让我很迷惑地方

  int x(0);
  int y = 0;
  int z{0};
  int c = {0};

   c++通常把c = {0}这种初始化方式看成和z{0}一样,那么x(0)y = 0又有什么区别呢?,对于基本类型来说没有任何区别,对于自定义类型则不一样:

Widget w1(2);   //调用的默认构造函数
Widget w2 = w1; //调用的是拷贝构造函数
w1 = w2;        //调用的赋值操作符

   而{}这种初始化方式调用的是带有initializer_list的构造函数,这就是{}()的一个区别之处。

   C++11中我们可以给类的成员变量之间赋初值,这个特性还是很友好的,但是你不能使用x(0)这样的初始化方式。

int x{0};
int y = 0;
int z(0);   //error

   这就是{}()的另外一个区别之处。在C++11中引入的std::atomic是一个不可拷贝的对象,对于它的初始化是不能利用 y = 0这种形式的,因为它会调用默认拷贝构造函数。

std::atomic<int> ail{0};
std::atomic<int> ai3 = 0;   //error

   y = 0x(0) 这两种形式都有其不适用的地方,而z{0}这种形式则都可以适用,这也就是为什么在c++11中这种初始化方式被称为统一初始化的原因吧。除此之外统一初始化的这种方式还可以避免窄化的转换。

double x,y,z;
int sum1{x + y + z};    //error 窄化转换,报错
int sum2 = x + y + z;   //fine
int sum3(x + y + z);    //fine

   使用统一初始化的方式还有另外一个好处就是避免了C++复杂的语法分析。

Widget w2();    // 对于C++编译器来说需要区别这是一个函数声明还是一个变量的初始化

   如果上面的w2使用了{}统一初始化的方式就避免了复杂的语法分析问题的产生。按照上面的分析我应该鼓励大家使用统一初始化,毕竟上面的这些优点还是很赞的,话又说回来了,C++什么时候有过没啥坑的特性了统一初始化也是一样,有一些不足之处,在Item2中介绍过对于统一初始化auto得到的类型是std::initializer_list类型,此外还容易和普通的初始化方式产生不一致的行为。

class Widget {
 public:
    Widget(int i,bool b);
    Widget(int i,double b);
    Widget(std::initializer_list<long double> il);
};

Widget(10,true);    //调用的是第一个构造函数,
Widget{10,true};    //按理应该是调用第一个构造函数,但是现在却调用了带初始化列表的构造函数

   究其原因就是统一初始化是允许宽化转换的,所以上面10true都转换成long double了。更有甚者编译器会优先匹配std::initializer_list即使不成功也会去匹配。

class Widget {
 public:
    Widget(int i,bool b);
    Widget(int i,double b);
    Widget(std::initializer_list<bool> il);
};

Widget w{10,5.0};   //error 10窄化转换成bool了

   那岂不是只要使用了{}进行统一初始化都会匹配带有std::initializer_list的构造函数吗?,也不完全是这样因为int可以隐式转换成bool所以会优先匹配,如果没法转换了,那么还是会老老实实匹配普通的构造函数的。

class Widget {
 public:
    Widget(int i,bool b);
    Widget(int i,double b);
    Widget(std::initializer_list<string> il);
};

Widget w{10,5.0};   //匹配第一个构造函数,因为10和5.0都无法隐式转换成string

看完了上面的例子后,再来看一个边界情况的例子:

class Widget {
 public:
    Widget();
    Widget(std::initializer_list<int> il);
};

Widget w1;      // 调用默认的构造函数
Widget w2{}     // 也是调用默认的构造函数

   这下有点晕了,上面不是说了,在使用{}这种方式进行初始化的时候选择的不是带有std::initializer_list的构造函数吗?。这里怎么和上面说的不一致呢? 没办法,这是一个特例,如果你想让他调用带有初始化列表的构造函数,你需要像下面这样来调用它:

Widget w3({});
Widget w4{{}};  // ditto

在我们知道了{}()的一些坑后,我们可以去看看标准库中的vector。

std::vector<int> v1(10,20); //使用的是非初始化列表的版本,10个元素,每个元素的值是20
std::vector<int> v2{10,20}; //使用的带初始化列表的版本,2个元素,值分别是10,20

   如果不知道{}()的一些不同的话,很容易认为上面两种形式是一致的,尽管{}()初始化的方式有很多的不同,使得我们在使用的过程中会造成一定的困扰,但是只要我们保持一致这种困扰就会少了许多,避免{}()初始化混杂在一起。
    在一个模版中对于{}()的选择更是无迹可寻,例如下面这个模版

  template<typename T,                // type of object to create
            typename... Ts>            // types of arguments to use
   void doSomeWork(Ts&&... params)
   {
     // create local T object from params...
... }

对于上面的模版的函数体,可以替换成如下两种形式,但是对于传入的不同参数就会产生不可预期的结果

T localObject(std::forward<Ts>(params)...);
T localObject{std::forward<Ts>(params)...}; 

// 如果此时传入下面这代代码:
std::vector<int> v;
...
doSomeWork<std::vector<int>>(10, 20);
如果使用第一种方式就是创建10个元素,每个元素的值是20,如果是第二种形式就是创建两个元素1020

   对于上面这种情况,模版的作者其实也不知道预期的结果应该是什么样的,只有调用者是知道的,对于这类问题实在是没有很好的解决方案,只能通过注释的方式表明,标准库中的std::make_sharedstd::make_unique具有同样的问题,他们使用()初始化、并在代码中进行了注释。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值