《Effective Morden C++》Item 7: Distinguish between () and {} when creating objects

引子

从本Item开始,我们就进入了第二个章节,本章节,S.M.通过对比C++的新老feature来鼓励大家使用new features. 本Item先从对象初始化开始。

正文

1.基础

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;          // call default constructor
Widget w2 = w1;     // not an assignment; calls copy ctor
w1 = w2;            // an assignment; calls copy operator=

那么对于{}()两种初始化又有什么区别呢,我们下面开始详细讨论这个故事。

2.{}初始化的基本功能

首先,我们先聊点C++11。在C++11里面,开发人员提出了一个概念 uniform initialization: 希望有一种统一的语法能够在任何情况下初始化一个对象。这个思想的一个实现就是{}初始化(尽管不完美)。

通过引入{}初始化,我们可以做如下初始化操作(C++11之前是难以实现的)。例如,直接初始化一个容器:

std::vector<int> v{ 1, 3, 5 }; // v's initial content is 1, 3, 5

可以给类中的非静态成员赋值,之前只能用=而不能用()完成:

class Widget {
  ...
private:
  int x{ 0 };    // fine, x's default value is 0
  int y = 0;     // also fine
  int z(0);      // error!
};

此外,还可以给一些不能复制的变量初始化:

std::atomic<int> ai1{ 0 };  // fine
std::atomic<int> ai2(0);    // fine
std::atomic<int> ai3 = 0;   // error!

看了上面的例子,大家就能理解为什么说是uniform initialization了.

3. {}初始化的特殊特性

当然了,{}初始化也有自己的特殊之处:

首先是对于内置类型禁止窄化转换,而()=则允许这一点:

double x, y, z;
...

int sum1{ x + y + z };      // error! sum of doubles may 
                            // not be expressible as int

int sum2(x + y + z);        // okay
int sum3 = x + y + z;       // ditto

还有一个好处是免疫C++中最令人烦恼的歧义:most vexing parse.

Widget w2();    //most vexing parse! declares a function named w2 that returns a Widget! 

但是如果把上面的()换成{},则变成了声明了一个Widget对象,且初始化参数为空。

由此可见,{}初始化除了统一以外,还有如上两个优点。所以大部分情况,我们都推荐用{}初始化。

4.{}初始化的缺点

当然{}初始化还是有一些坑的,尽管需要比较特殊的条件。但是并不是意味着开发者遇不到。我们下面也逐一介绍。

{}初始化,std::initializer_list和构造函数重载(overload)同时出现时,会出现奇特的情况。例如在Item 2里面我们提到过当auto关键字声明的对象使用{}初始化时,那么类型会被推导成std::initializer_list,哪怕此时存在一个更匹配的类型。在构造函数里面也会出现类似的情况。

事实上,当没有声明std::initializer_list相关的构造函数时,{}()调用同样的构造函数。但是如果存在至少一个以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); // added
  ...
};

Widget w1(10, true);    // calls first ctor
Widget w2{10, true};    // uses braces, but now calls std::initializer_list ctor
                        // (10 and true convert to long double)

Widget w3(10, 5.0);     // calls second ctor
Widget w4{10, 5.0};     // uses braces, but now calls std::initializer_list ctor
                        // (10 and 5.0 convert to long double)

注意到上面的w2w4,由于使用{}初始化,因此都调用了std::initializer_list构造函数,哪怕需要隐式转换,哪怕有比它更合适的其他构造函数。

甚至,更极端地,在一些情况下,连拷贝构造函数和move构造函数,也会由于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;   // convert Widget to float
  ... 
};

Widget w5(w4);              // uses parens, calls copy ctor

Widget w6{w4};              // uses braces, calls std::initializer_list ctor
                            // (w4 converts to float, and float converts to long double)

Widget w7(std::move(w4));   // uses parens, calls move ctor

Widget w8{std::move(w4)};   // uses braces, calls std::initializer_list ctor
                            // (for same reason as w6)

因为在使用{}初始化时,编译器过于偏爱std::initializer_list构造器,因此会想尽一切办法使用它:先将Widget对象转换成float类型,再调用std::initializer_list构造器,最后将float类型又隐式转换成long double类型完成构造!这显然是十分不合理,不自然的。

不仅仅如此,还有更糟糕的事情,例如:

class Widget {
public:
  Widget(int i, bool b);                     
  Widget(int i, double d);                   
  Widget(std::initializer_list<bool> il);    // element type is now bool
...                                          // no implicit conversion funcs
};
Widget w{10, 5.0};                           // error! requires narrowing conversions

上面我们将std::initializer_list参数换成bool,这样在构造w时,还是优先调用std::initializer_list构造器,发现需要将int 10double 5.0窄化转换成bool类型,而窄化转换在{}初始化中是被禁止的。此时会出现编译错误,尽管存在其他合法的构造函数!

按照这样说,岂不是{}初始化都会匹配std::initializer_list构造函数(如果存在的话)了么?也不全是,如果没法将{}中的变量类型转化成std::initializer_list中的参数类型时,则会使用其他构造函数。例如:

class Widget {
public:
  Widget(int i, bool b);              
  Widget(int i, double d);

  // std::initializer_list element type is now std::string
  Widget(std::initializer_list<std::string> il);
  ... // no implicit conversion funcs
}


Widget w1(10, true);    // uses parens, still calls first ctor
Widget w2{10, true};    // uses braces, now calls first ctor
Widget w3(10, 5.0);     // uses parens, still calls second ctor
Widget w4{10, 5.0};     // uses braces, now calls second ctor

由于不存在intdoublestd::string的转换,因此编译器老老实实地匹配了其他构造函数。

由此可见在涉及{}初始化和std::initializer_list的场景一定要小心慎重。

5.{}初始化的其他主意事项

最后我们看几个其他比较特殊的cases。

1.当std::initializer_list构造函数和默认构造函数同时存在时,如果你使用了{}初始化且内部没有其他参数,那么这个时候编译器怎么对待这次构造呢。事实上,我们有:

class Widget {
public:
  Widget();                                 // default cto  
  Widget(std::initializer_list<int> il);    // std::initializer_list ctor
  ...                                       // no implicit conversion funcs
};

Widget w1;          // calls default ctor
Widget w2{};        // also calls default ctor
Widget w3();        // most vexing parse! declares a function!

如果你想让编译器使用std::initializer_list构造器,你应当像下面这样

Widget w4({}); // calls std::initializer_list ctor with empty list

我们需要注意到,如果将上面的()初始化换成{}初始化而其他的不变,编译器会认为你是调用了含有一个元素{}std::initializer_list构造器,而不是空的std::initializer_list构造器,具体的讨论见这个博客

Widget w4({}); // the constructor is called with a one-element     
               // std::initializer_list, not an empty one. 

第二个特殊情况是,在使用std::vector<numeric type>时,且传入参数为两个时,()初始化和{}初始化表现出截然不同的特性,这一点需要牢记:

std::vector<int> v1(10, 20);     // use non-std::initializer_list 
                                 // ctor: create 10-element
                                 // std::vector, all elements have
                                 // value of 20

std::vector<int> v2{10, 20};     // use std::initializer_list ctor: 
                                 // create 2-element std::vector,
                                 // element values are 10 and 20

最后一点,由于上面的几个特殊cases,导致{}初始化和()初始化在涉及到模板时,此时的选择更是无迹可寻。例如:

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对象,例如

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

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

两种方法都是可能的,而且对于外部调用者我们无法知道函数的创造者使用了哪个初始化方法。这样,当我们运行如下代码时:

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

会出现两种情况:可能创建了一个含有10个元素,每个都是20的对象,也可能创建了含有两个元素(10和20)的对象。而这完全由函数的创造者决定,我们调用人员无能为力,这显然是不能接受的。对于这种问题我们没有好的解决方案,只能在注释中写明。例如std::make_sharedstd::make_unique就使用了()初始化并在注释中说明了这一点。

总结

说了这么多,终于到总结部分了:

1.{}初始化是最广泛的初始化语法,它禁止窄化转换,并且对most vexing parse免疫。
2.如果用std::initializer_list重载了构造函数,那么{}初始化会尽量匹配std::initializer_list构造函数,哪怕需要做隐式转换,哪怕有其他更匹配的构造函数。这在一些极端情况可能导致编译错误。
3.(){}初始化在涉及到恰有两个参数的数字类型std::vector时会给出不同的构造结果,这一点尤要注意。
4.模板里面究竟使用()初始化还是{}初始化是一个很有挑战的事情,并没有很好的解决办法,只能再函数接口注释里写明白。

If you’re an experienced C++ programmer and are anything like me, you initially approached C++11 thinking, “Yes, yes, I get it. It’s C++, only more so.” But as you learned more, you were surprised by the scope of the changes. auto declarations, range-based for loops, lambda expressions, and rvalue references change the face of C++, to say nothing of the new concurrency features. And then there are the idiomatic changes. 0 and typedefs are out, nullptr and alias declarations are in. Enums should now be scoped. Smart pointers are now preferable to built-in ones. Moving objects is normally better than copying them. There’s a lot to learn about C++11, not to mention C++14. More importantly, there’s a lot to learn about making effective use of the new capabilities. If you need basic information about “modern” C++ features, resources abound, but if you’re looking for guidance on how to employ the features to create software that’s correct, efficient, maintainable, and portable, the search is more challenging. That’s where this book comes in. It’s devoted not to describing the features of C++11 and C++14, but instead to their effective application. The information in the book is broken into guidelines called Items. Want to understand the various forms of type deduction? Or know when (and when not) to use auto declarations? Are you interested in why const member functions should be thread safe, how to implement the Pimpl Idiom using std::unique_ptr, why you should avoid default capture modes in lambda expressions, or the differences between std::atomic and volatile? The answers are all here. Furthermore, they’re platform-independent, Standards-conformant answers. This is a book about portable C++.
### 回答1: 《现代控制系统》第13版解答是一本帮助学习者掌握现代控制系统基础知识和技能的重要参考书。本书主要介绍了控制系统的基本概念、模型和各种控制方法,视觉系统和数字信号处理。本书重点介绍了基于状态空间的设计方法和反馈控制理论,以及自适应控制、模糊逻辑控制和神经网络控制等现代控制技术。 全书主要包括了控制系统分析基础、传递函数模型、状态空间模型、自动调节器设计、采样控制系统、稳定性分析和设计、频率分析与设计、根轨迹设计方法、神经网络控制、模糊控制、数字控制和非线性控制等内容。 《现代控制系统》第13版解答还包括大量的习题和例题,以帮助学习者加深对各种控制方法的理解和掌握,同时通过习题和例题也能够检验学习成果,提高自己的解决问题的能力。总之,这本书是学习现代控制系统的必备读物,对于感兴趣的读者来说是一本非常重要的参考书。 ### 回答2: 现代控制系统第13版解决方案是一个包含每章问题解答的资源。此书是用于控制理论和实践的优秀教材,但是在学习过程中,同学们可能会发现一些问题难以理解或解决,因此解决方案提供了对应的解答,帮助同学们更好地理解和掌握控制系统的知识。 该解决方案不仅提供了每章末尾的问题解答,也提供了一些实用的辅助资源,如MATLAB代码、实验指导、作业指导和简短的概述,这些资料可以帮助同学们更好地掌握控制理论,并更好地应用到实践中。 此外,该解决方案还提供了一些挑战性问题,供那些想要将自己的知识进一步拓展的同学们挑战。通过解决这些问题,同学们可以更深入地了解控制系统的原理和应用,以及如何将控制理论运用到实际工程问题中。 总之,现代控制系统第13版解决方案是一个非常有价值的资源,它可以帮助学生更好地理解和掌握控制系统的知识和应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值