Item7 Distinguish between () and {} when creating objects

[复制链接]

该用户从未签到

759

主题

763

帖子

4660

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
4660
跳转到指定楼层
楼主
发表于 2018-6-4 15:25:42 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

想要查看内容赶紧注册登陆吧!

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
在引入C++11后变量的初始化方式多种多样,对于每种初始化的方式的区别和联系是一个让我很迷惑地方
  1. int x(0);
  2.   int y = 0;
  3.   int z{0};
  4.   int c = {0};
复制代码
c++通常把c = {0}这种初始化方式看成和z{0}一样,那么x(0)和y = 0又有什么区别呢?,对于基本类型来说没有任何区别,对于自定义类型则不一样:
  1. Widget w1(2);   //调用的默认构造函数
  2. Widget w2 = w1; //调用的是拷贝构造函数
  3. w1 = w2;        //调用的赋值操作符
复制代码
而{}这种初始化方式调用的是带有initializer_list的构造函数,这就是{}和()的一个区别之处。   C++11中我们可以给类的成员变量之间赋初值,这个特性还是很友好的,但是你不能使用x(0)这样的初始化方式。
  1. int x{0};
  2. int y = 0;
  3. int z(0);   //error
复制代码
这就是{}和()的另外一个区别之处。在C++11中引入的std::atomic是一个不可拷贝的对象,对于它的初始化是不能利用 y = 0这种形式的,因为它会调用默认拷贝构造函数。
  1. std::atomic<int> ail{0};
  2. std::atomic<int> ai3 = 0;   //error
复制代码

y = 0 和 x(0) 这两种形式都有其不适用的地方,而z{0}这种形式则都可以适用,这也就是为什么在c++11中这种初始化方式被称为统一初始化的原因吧。除此之外统一初始化的这种方式还可以避免窄化的转换。
  1. double x,y,z;
  2. int sum1{x + y + z};    //error 窄化转换,报错
  3. int sum2 = x + y + z;   //fine
  4. int sum3(x + y + z);    //fine
复制代码
使用统一初始化的方式还有另外一个好处就是避免了C++复杂的语法分析。
  1. Widget w2();    // 对于C++编译器来说需要区别这是一个函数声明还是一个变量的初始化
复制代码
如果上面的w2使用了{}统一初始化的方式就避免了复杂的语法分析问题的产生。按照上面的分析我应该鼓励大家使用统一初始化,毕竟上面的这些优点还是很赞的,话又说回来了,C++什么时候有过没啥坑的特性了统一初始化也是一样,有一些不足之处,在Item2中介绍过对于统一初始化auto得到的类型是std::initializer_list类型,此外还容易和普通的初始化方式产生不一致的行为。
  1. class Widget {
  2. public:
  3.     Widget(int i,bool b);
  4.     Widget(int i,double b);
  5.     Widget(std::initializer_list<long double> il);
  6. };

  7. Widget(10,true);    //调用的是第一个构造函数,
  8. Widget{10,true};    //按理应该是调用第一个构造函数,但是现在却调用了带初始化列表的构造函数
复制代码
究其原因就是统一初始化是允许宽化转换的,所以上面10和true都转换成long double了。更有甚者编译器会优先匹配std::initializer_list即使不成功也会去匹配。
  1. class Widget {
  2. public:
  3.     Widget(int i,bool b);
  4.     Widget(int i,double b);
  5.     Widget(std::initializer_list<bool> il);
  6. };

  7. Widget w{10,5.0};   //error 10窄化转换成bool了
复制代码
那岂不是只要使用了{}进行统一初始化都会匹配带有std::initializer_list的构造函数吗?,也不完全是这样因为int可以隐式转换成bool所以会优先匹配,如果没法转换了,那么还是会老老实实匹配普通的构造函数的。
  1. class Widget {
  2. public:
  3.     Widget(int i,bool b);
  4.     Widget(int i,double b);
  5.     Widget(std::initializer_list<string> il);
  6. };

  7. Widget w{10,5.0};   //匹配第一个构造函数,因为10和5.0都无法隐式转换成string
复制代码
看完了上面的例子后,再来看一个边界情况的例子:
  1. class Widget {
  2. public:
  3.     Widget();
  4.     Widget(std::initializer_list<int> il);
  5. };

  6. Widget w1;      // 调用默认的构造函数
  7. Widget w2{}     // 也是调用默认的构造函数
复制代码
这下有点晕了,上面不是说了,在使用{}这种方式进行初始化的时候选择的不是带有std::initializer_list的构造函数吗?。这里怎么和上面说的不一致呢? 没办法,这是一个特例,如果你想让他调用带有初始化列表的构造函数,你需要像下面这样来调用它:
  1. Widget w3({});
  2. Widget w4{{}};  // ditto
复制代码
在我们知道了{}和()的一些坑后,我们可以去看看标准库中的vector。
  1. std::vector<int> v1(10,20); //使用的是非初始化列表的版本,10个元素,每个元素的值是20
  2. std::vector<int> v2{10,20}; //使用的带初始化列表的版本,2个元素,值分别是10,20
复制代码
如果不知道{}和()的一些不同的话,很容易认为上面两种形式是一致的,尽管{}和()初始化的方式有很多的不同,使得我们在使用的过程中会造成一定的困扰,但是只要我们保持一致这种困扰就会少了许多,避免{}和()初始化混杂在一起。
    在一个模版中对于{}和()的选择更是无迹可寻,例如下面这个模版
  1. template<typename T,                // type of object to create
  2.             typename... Ts>            // types of arguments to use
  3.    void doSomeWork(Ts&&... params)
  4.    {
  5.      // create local T object from params...
  6. ... }
复制代码
对于上面的模版的函数体,可以替换成如下两种形式,但是对于传入的不同参数就会产生不可预期的结果
  1. T localObject(std::forward<Ts>(params)...);
  2. T localObject{std::forward<Ts>(params)...};

  3. // 如果此时传入下面这代代码:
  4. std::vector<int> v;
  5. ...
  6. doSomeWork<std::vector<int>>(10, 20);
  7. 如果使用第一种方式就是创建10个元素,每个元素的值是20,如果是第二种形式就是创建两个元素10和20
复制代码
对于上面这种情况,模版的作者其实也不知道预期的结果应该是什么样的,只有调用者是知道的,对于这类问题实在是没有很好的解决方案,只能通过注释的方式表明,标准库中的std::make_shared、std::make_unique具有同样的问题,他们使用()初始化、并在代码中进行了注释。



分享到:  QQ好友和群QQ好友和群
收藏收藏
回复

使用道具 举报

快速回复高级模式
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表