当前位置: 代码迷 >> 综合 >> Effective Modern C++ Item 7 在创建对象时注意区分()和{}
  详细解决方案

Effective Modern C++ Item 7 在创建对象时注意区分()和{}

热度:37   发布时间:2024-01-29 04:58:00.0

在C++11中,一个变量初始化的表达方式有太多种,如下所示:

int x(0);           //初始化值在小括号内
int y = 0;          //初始化值在等号后
int z{0};           //初始化值在大括号内
int z = {0};        //使用等号和大括号来指定初始化值,被视作只有大括号//但由于有等号的存在,往往会让人误以为赋值了,实际没有。

int这样的内置类型,初始化和赋值只是学术之争,没太多区别。但是自定义对象差别就很大了,示例代码如下:

Widget w1;          //调用的是默认构造函数
Widget w2 = w1;     //并非赋值,调用的是复制构造函数
w1 = w2;            //并非赋值,调用的是复制赋值运算符

大括号初始化的特点

C++11引入了统一初始化:单一的、至少从概念上可以用于一切场合、表达 一切意思的初始化。

“统一初始化”是目的,“大括号初始化”是表象。

//可以办到之前不能办到的事情
std::vector<int>  v{1,3,5};         //v的初始内容为1,3,5
//可以在非静态成员指定默认初始化值
class Widget {
...
private:int x {0};                      //可行,x的默认值为0int y = 0;                      //也可行int z(0);                       //不行!
}
//不可复制的对象也可以用大括号和小括号初始化
std::atomic<int> ai1{0};            //可行
std::atomic<int> ai2(0);            //可行
std::atomic<int> ai3 = 0;           //不行!

由上可知,只有大括号初始化的方式能用于各种场景,所以又被称之为统一初始化

大括号初始化的规则

  • 大括号初始化禁止内建型别之间进行隐式窄化型别转换。
double x, y, z;
...
int sum1{x + y + z};                //错误!double型别之和无法使用int表达
int sum2(x + y + z);                //没问题,表达式被截断为int
int sum3 = x + y + z;               //没问题,表达式被截断为int
  • 大括号初始化可以避开最令人苦恼的解析语法。

    这个规则源于C++的一个规定:

    任何能够解析为声明的都要解析为声明

    这会带来一个副作用,示例代码如下:

    Widget w1(10);                  //调用Widget的构造函数,传入形参10
    Widget w2();                    //最令人苦恼的解析语法!//这个语句声明了一个名为w2、返回了Widget型别对象的函数!
    Widget w3{};                    //调用没有形参的Widget构造函数!对解析为声明的陷阱免疫!
    

大括号初始化不合适的场景

大括号初始化不合适的场景主要体现在大括号std::initializer_list构造函数重载之间的纠葛。这三者之间在解析的时候,容易出现不是你觉得而是编译器觉得的问题

而且从结果上来看,很容易引起越喜欢使用auto,恐怕就会越来越对大括号初始化排斥。

  • 当构造函数不具备std::initializer_list型别的时候,大括号和小括号是等价的
class Widget {
public:Widget(int i, bool b);          //构造函数不具备任何一个std::initializer_list型别Widget(int i, double d);...
};Widget w1(10, true);                //调用第一个构造函数
Widget w2{10, true};                //调用第一个构造函数
Widget w3(10, 5.0);                 //调用第二个构造函数
Widget w4{10, 5.0};                 //调用第二个构造函数
  • 但是如果构造函数里生命了任何一个具备std::initializer_list的形参,那么编译器会特别强烈的优先选用带有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 w1(10, true);                //调用第一个构造函数
Widget w2{10, true};                //调用第三个构造函数,10,true强转为long double
Widget w3(10, 5.0);                 //调用第二个构造函数
Widget w4{10, 5.0};                 //调用第三个构造函数,10,5.0强转为long double
Widget w5(w4);                      //调用复制构造函数
Widget w6{w4};                      //调用第三个构造函数,w4的返回值被强转成float,//然后再被强转成long double,再去匹配
Widget w7(std::move(w4));           //调用移动构造函数
Widget w8{std::move(w4)};           //调用第三个构造函数,和w6一样的结果

更有甚者,在无法进行强转的时候,编译器会拒绝能够匹配上的构造函数,例如:

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的构造函数,但是由于10和5.0转化成bool会造成窄化型别转换,这一点不符合大括号初始化的规则,所以编译通不过

但如果把上述std::initializer_list<bool>换成std::initializer_list<std::string>的时候,一切会正常。因为只有在找不到任何办法把大括号初始化物中实参转换成模板型别的时候,编译器才会放弃使用std::initializer_list的初始化。

  • 边界情况讨论:空大括号和默认构造函数

结论:空大括号表示没有实参,调用默认构造函数

class Widget {
public:Widget();Widget(std::initializer_list<int> il);     //容器元素型别现在是int...                                        //并没有隐式强制型别转换函数
};Widget w1;              //调用默认构造函数
Widget w2{};            //调用默认构造函数
Widget w3();            //最令人苦恼的语法,这里变成了声明
Widget w4({});          //带有std::initializer_list型别的构造函数,传入空的initializer_list
Widget w5{{}};          //同上

以上介绍完所有规则,下面介绍一个日常编码中会遇到的常见问题:

介绍常见例子以及使用场景

STL中的std::vector<T>就会遇到这种问题。
在C++11中,std::vector<T>就拥有std::initializer_list作为构造参数的构造函数:

// 先来看std::vector构造函数声明
// ******************
// 默认构造函数
// Constructs an empty container, with no elements.
explicit vector (const allocator_type& alloc = allocator_type());// fill constructor
// Constructs a container with n elements. Each element is a copy of val (if provided).
explicit vector (size_type n);vector (size_type n, const value_type& val,const allocator_type& alloc = allocator_type());
......
// initializer list constructor
// Constructs a container with a copy of each of the elements in il, in the same order.
vector (initializer_list<value_type> il,const allocator_type& alloc = allocator_type());// *****************
// 调用了fill constructor
std::vector<int> v1(10, 20); 
// 调用了initializer list constructor
std::vector<int> v1{10, 20};

这里{}和()不一致的地方值得注意。

  • 当你是一个类作者,需要避免将构造函数中声明了std::initializer_list型别的形参。最好把构造函数设计成()和{}的体现一致的情况。从这个角度看来,一般把std::vector的接口设计视为败笔,应该从中汲取教训,避免同类行为。

所以当你在一个类中新增构造函数为std::initializer_list型别的时候,心中需要格外小心,避免std::initializer_list覆盖其他构造函数的情况。

当你是一个开发模板的程序猿,那么创建对象的时候,选用小括号还是大括号的问题,会变得非常的头疼。总体来说,选用什么都不好。

具体例子介绍如下:

template<typename T,typename... TS>
void doSomeWork(TS&&... params)
{// 利用params创建局部对象TT localObject(std::forward<Ts>(params)...);     //选择小括号T localObject{std::forward<Ts>(params)...};     //选择大括号
}

考虑一下调用代码:

std::vector<int> v;
...
doSomeWork<std::vector<int>>(10, 20);

如果采用小括号的方式,那么最终这个对象会包含10个元素的vector,如果采用大括号的方式,会得到一个2个元素的vector。那么究竟谁对谁错呢。其实只有调用者才有决定权。

而这种情况其实在标准库函数std::make_uniquestd::make_shared中就有这样的考量。他们的做法是在内部使用了小括号,并且用文档的形式广而告知。

要点速记
1. 大括号初始化可以应用的语境最为广泛,可以阻止隐式窄化类型转换,还对最令人苦恼的解析语法免疫。
2. 在构造函数重载决议期间,只要有任何可能性,大括号初始化就会与带有std::initializer_list型别的形参相匹配,即使其他重载版本有着更加匹配的形参表。
3. 使用小括号还是大括号,会造成结果大相径庭的例子:使用两个实参来创建一个std::vector<数值型别>对象。
4. 在模板内容进行对象创建时,到底应该用小括号还是大括号会成为一个棘手问题。
  相关解决方案