章节3:拥抱现代C++
item 7:区别()与{ }两种对象创建方法
在C++中,对象的创建方法可以有以下几种:
int x(0); //① 使用括号初始化
int y = 0; //② 使用等号初始化
int z { 0 }; /③ 用中括号初始化
int z = {0}; //本质还是中括号初始化;本文不予考虑;
常见误解之一:②处发生了复赋值。
解释:对于内置类型而言(比如int),二者的差距是学术上的,可以忽略。对于自定用户自定义类型而言,初始化与赋值不可同日而语,因为二者调用不同的函数:
widget w1; //调用默认构造函数;
widget w2 = w1; //不是赋值;调用拷贝构造函数;
w1 = w2; //是赋值;调用拷贝运算符 =
尽管有许多的初始化语法,C++98在某些时候还是不能产生我们想要的初始化。比如:直接创造(create)一个拥有特定值的STL容器是不可能的。(含糊)
为了解决这种多种初始化语法的含糊场景、创造一个可以覆盖所有初始化场景的语法,C++11引入了“通用初始化”:中括号初始化(braced initialization)。从概念上来说,“中括号初始化”这一语法结构可以用在任何地方,表达任何东西。使用这一语法,容器的初始化可以这么写:
std::vector<int> v{1,2,3}; //v的初始化内容为:1,2,3
class {
……
private:
int x{0}; //x的初始化值为0;
int y = 0; //作用同上;
int z(0); //error!
}
C++中还引入了这样的新特性:对于非静态成员而言,{ }可以指定默认初始值
代码如上 。即使是不可拷贝的对象,如 std::atomic<int> ai1 {0};
也可以用中括号初始化,代码如上。需要尤为注意的是:中括号禁止隐式缩小转换implicit narrowing conversions
。代码如下:
double x,y,z;
……
int sum1{x+y+z}; //error! sun of doubles may not be //expressible as int
int sum1(x+y+z);//ok,value of expression truncated to in int
int sum3 = x+y+z;//ditto
与此同时,(),= 确允许这样做。否则原有的合法代码就会出问题。代码如上。
其次,{ } 的优势还体现在它对C++谜之解析的免疫 原文:most vexing parse
。C++有这样的一条规则:anything that can be parsed as a declaration must be interpreted as one
这一规则带来的直接副作用就是“谜之解析”,具体表现如下:
widget w1(10); //①以10为参数调用构造函数;
widget w2(); //②调用默认构造函数;
widget w3{}; //③调用默认构造函数;
②出代码的目的是比较清晰的,但是由于C++的规则,实际上发生的事是:声明了一个名为w2的函数,函数签名为:widget()[^1] 。但是书写成③的形式则可以完成默认构造。
总结上文,不难得出结论:除了需要narrowing conversion
的场合,其他时候应当尽可能的使用{ } 进行初始化 。那么,标题应该改为“优先选择 { }初始化”?
中括号初始化当然是有缺点的:initializer_lists和构造器重载之间的苟且关系
孩子没娘,说来话长。且听Meyers徐徐道来。
重温item2[^2] ,当使用auto推到{ }构造时auto x = {1,2,3};
,x所得并非基础类型int,而是模板 std::initializer_lists。这只是引子,由这一现象继续深挖下去。
当构造函数不存在std::initializer_lists时,(),{ }表现是一致的。如下:
class widget{
public:
widget(int i, bool b);
widget(int i, double d);
……
}
widget w1(10,true); //一号构造;
widget w2{10, true}; //ditto
widget w3{10,5.0}; //二号构造
上面这些东西并不难理解,单纯的构造函数重载而已。但是当std::initializer_lists横插一脚时,奸情发生了。如下:
class widget{
public:
widget(int i, bool b);
widget(int i, double d);
widget(std::initializer_list<long double> ld);
operator float() const;
……
}
widget w1(10,true); //as before;
widget w2{10, true}; //三号构造
widget w3{10,5.0}; //三号构造
widget w4{w4}; //三号构造
widget w5{std::move(w4)};//三号构造
我的Meyer![^3]不管是类型不符合,或是由类型转换强行分离调用者与构造函数之间的关系,只要构造函数中有参数std::initializer_lists,{ }初始化必然是优先调用它,哪怕是经过大量的类型转换。除非,构造函数是这样:
widget (std::initializer_list<std::string> str)
这样以来,std::string 与build_in type 明显不能再相互转换,此时{ }才恢复正常的构造函数调用——按照参数的匹配情况。
这时又存在一个额外情况:
class widget{
widget();
widget(std::initializer_list<int> il);
……
}
widget w1; //默认构造
widget w2{}; //默认构造!!
widget w3(); //most vexing parse
widget w4({}); //重载构造;
widget w5{{}}; //ditto
当调用构造函数且无参数时,统一调用默认构造函数 ,此时再想调用std::initializer_lists 需要采用w4,w5的形式。
应用。说了这么多语法的情况,无非是要用。可以用在那里嘞?看代码:
std::vector<int> v1(10,20);
//vector中有10个元素,每个元素都是20;
std::vector<int> v2{10,20};
//vector中有2个元素,分别时10,20;
收获。
一:大多数情况下,std::initializer_lists<int>不是compete other overloads, 而是overshadow others;
二:
():避免了auto出std::initializer_lists<int>;
分离了std::initializer_lists<int>构造和普通构造。
{}:immunity to c++ most vexing parse;
prohibition of narrowing conversion;
选择一个做日常使用,另一个则正在have to的时候使用;
————————————————————————————————
感觉比较长,翻译着玩。
***
[^1]: 函数签名(function signature),是指函数的返回值,形参。这是作者meyers 在书中的规定。
[^2]: 指书籍 effective modern C++
[^3]: 谐音梗,妈耶 和 Meyers的部分发音类似 :laughing:
***