Effective Modern C++翻译系列之Item7

Item7:Distinguish between() and {} when creating objects.

依据你的看法,c++11中对象初始化的语法选择包含了太过繁杂导致的窘迫和令人困惑的混乱。一个全面的条例说道,初始化变量可能会用到圆括号,等号或者大括号:

int x(0); //初始化表达式在圆括号里

int y = 0; //初始化表达式在=后面

int z{ 0 }; //初始化表达式在大括号里

在很多情况下,同时使用大括号和等号也是有可能的:

int z = { 0 }; //初始化表达式用=和大括号

在这个条款的剩余部分,我一般会忽略掉等号+大括号版本的语法,因为c++对待它经常像仅有大括号版本。

“令人困惑的混乱”指出初始化中等号的使用常常会误导c++新手有赋值发生,尽管其实没有。对于像int这样的内建类型,区别仅在理论上,但是对于用户定义的类型,区分初始化和赋值是很重要的,因为不同的函数被调用。

 

Widget w1; //调用默认构造函数

Widget w2 = w1; //调用拷贝构造函数,没有赋值发生

w1 = w2; //赋值操作,调用拷贝赋值函数

 

尽管有数种初始化语法,有些情况下c++98中也没有办法表达需要的初始化。例如,不可能直接指示一个STL容器被一组指定的值创建(例如135)。

为了解决多样的初始化语法的困惑,以及语法不能应对所有初始化情景的事实,c++11介绍了统一初始化:一个单独初始化表达式语法,它至少在某方面能够被用在任何地方,表达任何东西。它基于大括号,因为这个原因我更喜欢用术语大括号初始化。统一初始化是一个想法,大括号初始化是句法的构造函数。

大括号初始化让你表达先前不能表达的情况。使用大括号,指定容器初始的内容很容易:

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

大括号也可以被用做指定非静态数据成员的默认初始化表达式的变量。这个新特性和=初始化语法的作用相同,但是圆括号初始化却不行:

Class Widget {

...

 

private:

int x{ 0 }; //很好,x的默认值是0

int y = 0; //也很好

int z( 0 ); //error

};

另一方面来说,不可拷贝的对象(例如,std::atomics-Item40)也许用圆括号或者大括号初始化,但不能用等号:

std::atomic<int> ai1{ 0 }; //很好

std::atomic<int> ai2( 0 ); //很好

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

因此很容易理解为什么大括号初始化被说是统一的。C++的三种指明初始化表达式的方法中,只有大括号初始化可以被用在任何地方。

大括号初始化一个新奇的特性是它禁止了内置类型隐式的narrowing 转换。如果大括号初始化表达式的值不保证能被转化(be expressible)为对象初始化的类型,编译器(are required to complain:)

double x,y,z;

...

int sum1{ x + y + z }; //errordouble类型的和也许不能被narrowing //conversionint

圆括号和等号初始化不检查narrowing conversion,因为它会违背太多的旧代码。

int sum2(x + y + z); //ok,表达式的值可以被转换为int

 

int sum3 = x + y + z; //ditto

另外一个大括号初始化的值得注意的特点是它对c++大多数令人烦恼的语法分析的免疫。C++的规则(任何能被分析为一个声明式的东西一定会被打断作为一个声明式)的次要影响:特别令人烦恼的语法分析折磨着程序员,他们想要缺省构造一个对象,但却疏忽地声明了一个函数。问题的根本是如果你想用一个参数调用构造函数,你能像下面这样做:

Widget w1(10); //用参数10调用Widget构造函数。

但如果你用无参调用Widget构造函数(用类似的语法),你将会声明一个函数而不是一个对象:

Widget w2(); //特别令人烦恼的语法分析!声明了一个名字为 //w2,返回一个Widget对象的函数。

参数列表使用大括号的话,函数不能被声明,所以用大括号默认构造一个对象就不会有这个问题。

Widget w3{}; //调用无参Widget构造函数。

因此大括号初始化有很多可以说的。它是能被用在最多种类的上下文中的语法,它避免了隐式的narrowing conversions,它免疫了c++中特别烦人的语法分析。多么好啊!那么为什么这个Item不命名为”完美的大括号初始化语法”?

大括号初始化的缺点是伴随着它的一些时不时令人惊讶的行为。这样的行为产生于和大括号初始化非常混乱的关系,std::initializer_list,和构造函数重载决议,它们之间的内部关系会导致看起来做一件事情的代码事实上做了其他事情。例如。Item2解释道当一个auto声明的变量用大括号初始化的时候,类型推断将会是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的构造函数。编译器一旦可以将一个使用大括号初始化的调用”翻译”为一个接受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);

...

};

Widget w2 w4将会使用新的构造函数来初始化,尽管在两个参数的类型匹配时,std::initializer_list元素的类型(long double)相比于其他两个构造函数是一种更不匹配的情况。看:
Widget w1(10,true); //使用圆括号,调用第一个构造函数。

 

Widget w2{10,true}; //使用大括号,但是现在会调用第三个构 //造函数(10true会转化为long double

 

Widget w3(10,5.0); //使用圆括号,调用第二个构造函数

 

Widget w4{10,5.0}; //使用大括号,但是现在调用第三个构造函 //数,(105.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;

...

};

 

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

 

Widget w6{w4}; //使用大括号,调用std::initializer_list构造函数

//w4转化为floatfloat转化为long 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);

... //没有隐式的转换函数

};

 

Widget w{10,5.0}; //error!需要narrowing conversions

 

这里,编译器会忽略开始的两个构造函数(第二个构造函数在每个参数类型上都是精确匹配)并且尝试去调用std::initializer_list<bool>构造函数。调用这个构造函数转化一个int10)和一个double5.0)为bool。两个转化都是narrowing的(bool不能完全表示两个参数中的任何一个),并且narrowing conversions在大括号初始化中是被禁止的,所以这样的调用是有问题的,代码不通过编译。

只有没有办法将大括号初始列表中参数的类型转换为std::initializer_list的类型,编译器才会退回考虑普通的重载决议。例如,如果我们将std::initializer_list<bool>构造函数替换为std::initializer_list<std::string>构造函数,那些非std::initializer_list构造函数再次变成候选项,因为没有办法将intsbools转化为std::strings:

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);

 

...

};

 

Widget w1; //调用默认构造函数

Widget w2{ }; //调用默认构造函数

Widget w3(); //声明一个函数

 

如果你想用一个空的std::initializer_list调用std::initializer_list构造函数,在大括号或者圆括号中传入一对空的大括号:

 

Widget w4({}); //调用std::initializer_list构造函数

 

Widget w5{{}}; //一样

 

 

现在,关于一些看起来不可思议的关于大括号初始化,std::initializer_list,和构造函数重载的规则在你脑子里冒泡,你也许想知道,在每天的开发过程中,有多少信息是和上述规则有关的。也许比你想象的要多,因为直接影响的其中一个类就是std::vectorstd::vector有非std::initializer_list构造函数,该函数允许你指定容器中初始元素的个数和每个元素应该的值,但它也拥有一个std::initializer_list构造函数允许你指定容器中元素的初始值。如果你创建numeric类型(例如一个std::vector<int>)的std::vector并且你传入构造函数两个参数,你是用大括号还是圆括号围住这些参数会造成巨大的不同:

 

std::vector<int> v1( 10, 20 );   //十个元素的vector,每个元素的值都是20.

 

std::vector<int> v2{ 10, 20 };  //两个元素的vector,元素的值分别是1020.

 

但是让我们退回到std::vector和大括号,圆括号和构造函数重载决议的细节上。这个讨论中有两点需要注意。第一,作为一个类的创建者,你需要意识到如果你设置的重载函数中有一个或者多个有std::initializer_list参数的函数,客户代码使用大括号初始化可能只能看到有std::initializer_list参数的构造函数。结果是,最好将你的构造函数设置成重载的构造函数的调用不会被客户试用大括号还是圆括号初始化所影响。换句话来说,现在这个std::vector的设计当作反面教材,设计你自己的类的时候注意避开它们。

如果你有一个没有std::initializer_list参数的构造函数的类,然后你添加了一个,使用大括号初始化的客户代码也许会发现曾经决议调用非std::initializer_list构造函数如今却使用了新函数。当然,这样的事可能会发生在你添加一个新函数到一些重载函数的任何时间段发生:曾经决议为老的重载函数的调用现在也许开始调用新的一个函数。关于std::initializer_list构造函数的不同之处在于,它不仅和其他重载函数竞争,甚至遮住了它们,使得它们很难再被调用。所以在添加这样的重载函数时请慎重考虑。

第二点是,作为一个类的客户,你必须小心的选择大括号还是圆括号初始化。大多数开发者会将其中一种作为缺省的选择,只有当他们不得不使用另外一种的时候,才会使用它们。缺省使用大括号的人们被它无与伦比的适用性以及对narrowing conversion的禁止,和对c++烦人的语法分析的免疫所吸引。这些人明白在某些情况下(例如在创建一个vector时,给了元素的个数和每个元素的值),圆括号是必须的。另一方面,缺省使用圆括号的人们,被它和c++98语法传统的一致性,auto类型推断-std::initializer_list问题的避免,和他们创建对象调用不会不经意的被std::initializer_list构造函数抢先匹配这些特性所吸引。他们承认有时候只有大括号可以被使用(当用具体的值创建一个容器的时候)。没有共识说一个一定比另一个号,所以我的建议是选择一个并且持续使用它。

如果你是一个模板的作者,大括号和圆括号在关于对象创建上的关系更令人沮丧,因为一般来说,不可能知道哪一个应该被使用。例如,假设你想创建一个对象,用任意的类型和任意数量的参数。一个模板函数让这看起来更直观一点:

template<typename T,

 Typename... Ts>

void doSomeWork(Ts&&... params)

{

Create local T object from params...

}

有两种方法将伪代码替换为真正的代码(关于std::forward的信息去看item25:

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

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

考虑下面的调用代码:

Std::vector<int> v;

...

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

如果在创建localObjectdoSomeWork使用的是圆括号,那结果将是std::vector拥有10个元素。如果doSomeWork使用的是大括号,那结果将是std::vector拥有2个元素。哪个是正确的?doSomeWork的作者不知道,仅仅只有调用者才知道。

这正是标准库函数std::make_uniquestd::make_shared(item21)面临的问题。这些函数通过内部使用圆括号,并且将这个决定记录为它们的接口解决的。

 

 

Things to Remember

 

1.大括号初始化是一种应用十分广泛的语法,它避免了narrowing conversions,和免疫了c++的烦人的语法分析。

2.构造函数重载决议时,只要可能,大括号初始化会匹配std::initializer_list参数,甚至其他的构造函数提供了更好的匹配。

3.大括号和圆括号的选择会在哪个方面产生有意义的不同的一个例子是创建一个有两个参数的std::vector<numeric type>

4.在模板中创建对象时,对大括号和圆括号的选择是一个挑战。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值