0
点赞
收藏
分享

微信扫一扫

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

phpworkerman 2022-02-07 阅读 22
c++

文章目录

这次是对 Effective Modern C++ Item 7 的学习笔记。

初始化方式

C++11开始变量初始化方式有以下几种:

int x(0);      // initializer is in parentheses
int y = 0;     // initializer follows "="
int z{ 0 };    // initializer is in braces
int z = { 0 }; // initializer uses "=" and braces

其中第四种使用等号和花括号方式初始化变量通常认为和第三种花括号的方式相同。使用等号进行初始化可能会被认为是赋值操作,对于内置类型(比如 int),可以忽略它们的区别,但对于用户自定义类型,则需要区别:

Widget w1;      // call default constructor
Widget w2 = w1; // not an assignment; calls copy ctor
w1 = w2;        // an assignment; calls copy operator=

使用 {} 初始化被称为统一初始化(uniform initialization),期望能够统一应用在所有初始化场景(实际上也有缺陷,后文将介绍)。

() 和 = 初始化方式的限制

圆括号不能用于非静态成员变量的默认初始化:

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

// 编译报错信息
main.cpp:10:11: error: expected identifier before numeric constant
   10 |     int z(0);    // error!

成员变量 z 的初始化将会导致编译报错。

另外,C++中不能拷贝对象则不能使用等号初始化:

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

// 编译报错信息
main.cpp:5:24: error: use of deleted function ‘std::atomic<int>::atomic(const std::atomic<int>&)’
   std::atomic<int> a = 0;
                        ^

= 和 () 初始化都有使用的限制,可能就是 {} 初始化被称为统一初始化的原因吧。

{} 初始化的优势

统一初始化可以避免隐式窄化转换(narrowing conversions

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 (value of expression truncated to an int)
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 w1(10);  // call Widget ctor with argument 10
Widget w3{};    // calls Widget ctor with no args

{} 初始化的不足

除了 Item 2 介绍的 auto 变量类型声明使用统一初始化时候类型被推导成 std::initializer_list 的特点外,还存在统一初始化和其他初始化行为不一致的情况。

在没有 std::initializer_list 参数类型的构造函数时:

class Widget {
public:
Widget(int i, bool b);    // ctors not declaring
Widget(int i, double d);  // std::initializer_list params
...
}; 

Widget w1(10, true);  // calls first ctor
Widget w2{10, true};  // also calls first ctor
Widget w3(10, 5.0);   // calls second ctor
Widget w4{10, 5.0};   // also calls second ctor

在增加一个std::initializer_list 参数类型的构造函数时:

class Widget {
public:
Widget(int i, bool b);                              // as before
Widget(int i, double d);                            // as before
Widget(std::initializer_list<long double> il);      // added
...
};

Widget w1(10, true);    // uses parens and, as before, 
                        // 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);     // uses parens and, as before,
                        // 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)

这里,w2 和 w4 将会使用新增的构造函数(第3个构造函数)。但是很明显, non-std::initializer_list 参数类型构造函数比std::initializer_list 参数类型构造函数更加匹配。

更有甚者,拷贝和移动构造函数也能被 std::initializer_list 构造函数绑架:

class Widget {
public:
Widget(int i, bool b);    // as before
Widget(int i, double d);  // as before
Widget(std::initializer_list<long double> il);  // as before
operator float() const;   // convert 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 构造函数的决心很强,甚至导致编译报错,也没有匹配到普通的构造函数:

class Widget {
public:
Widget(int i, bool b);   // as before
Widget(int i, double d); // as before
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> 构造函数,但是需要将 int (10) 和 double (5.0) 转换为 bool 类型,这是窄化转化,将会失败(前面有解释),这里就导致错误。

只有花括号中参数无法转换为 std::initializer_list 中类型时,编译器才匹配普通函数:

class Widget {
public:
Widget(int i, bool b);    // as before
Widget(int i, double d);  // as before
// std::initializer_list element type is now std::string
Widget(std::initializer_list<std::string> il);
};

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

统一初始化除了存在上述问题,还有一些边界 case 需要处理下,看下面的例子:

class Widget {
public:
Widget();                              // default ctor
Widget(std::initializer_list<int> il); // std::initializer_list ctor

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

w2 将会调用默认构造函数,而没有调用选择调用 std::initializer_list 构造函数并将 list 设置为空,这是一个特例。如果希望调用 std::initializer_list 构造函数, 如下:

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

了解了以上 {} 和 () 的初始化的一些不足之后,我们再看下标准库的情况,对于新手,很容易以为下面两种方式创建的对象是相同的。

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                             

再看下面的例子:

  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 localObject(std::forward<Ts>(params)...); // using parens
T localObject{std::forward<Ts>(params)...}; // using braces

再考虑下面的调用代码:

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

如果 doSomeWork 采用圆括号的实现,结果将是 10 个元素的 std::vector。如果 doSomeWork 采用花括号的实现,结果将是 2 个元素的 std::vector

标准库中的 std::make_sharedstd::make_unique 使用圆括号初始化,并且在代码中做了注释。这类问题没有什么好的解决方案,只能在代码中添加注释来告知调用者。

举报

相关推荐

0 条评论