[]、()、{}

容器创建时[]{}

#include <iostream>
#include <vector>

int main() {
  std::vector<int> vec[2];
  vec[0].push_back(1);
  vec[1].push_back(2);
  //vec[0].push_back(1);
  vec[1].push_back(2);
  std::cout <<"vec[0].size():"<< vec[0].size() << std::endl;
  std::cout <<"vec[1].size():"<< vec[1].size() << std::endl;
  std::vector<int> tt_v{1,2,3};
  std::cout <<"tt_v.size():"<< tt_v.size() << std::endl;
  std::vector<int> t_vec = {1,2,3};
  std::cout <<"t_vec.size():"<< t_vec.size() << std::endl;
  return 0;
}

运行结果

[num]

这个代表申请多少个类似的vector,其他容器也是类似道理

{}

这个{}里面是放具体的的元素,这种写法一般是-std=c++11

创建对象时注意区分(){}

初始化值的方式包括小括号、使用等号、使用大括号,着实让人烟花缭乱。

  int x(0);  // 初始化在小括号中
  int y = 0; // 初始化在等号之后
  int z{0}; // 初始化在大括号内
  auto w{0}; // 这种初始化w推导为std::initializer_list

当然还有大括号加等于号的情况,通常在C++中会把他当成只有大括号的语法处理。
对于使用一个等号来初始化语句通常往往会让一个新手任务这里会发生一次赋值。对于int这样的內建型别来说初始化和赋值
的区别可有可无,但是对于用户自定义的型别,初始化和等于的背后调用的是不同的函数。

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

C++11引入了统一初始化:单一的,可以用于一切场合的初始化,就是大括号初始化。

std::vector<int> v{1, 3, 5}; // v的内容为1, 3, 5

大括号同样可以为非静态成员指定默认的初始化值,当然这里也可以用等于符号,但是却不能用小括号:

class Widget {
 private:
   int x{ 0 }; // 可行,x的默认值为0
   int y = 0; // 可以,最常见的用法
   int z(0); // 不行
}

不可复制的对象可以使用大括号和小括号,却不能使用等号=

std::atomic<int> ai1{ 0 }; // 可行
std::atomic<int> ai2(0); // 可行
std::atomic<int> ai3 = 0; //不可行
  1. 大括号的一项新的特性:
它禁止内建型别之间进行隐式窄化型别转换(narrowing cpnversion)

如果大括号的表达式无法保证能够采用进行初始化对象来表达,则代码编译不通过。

double x, y, z;
int sum1{ x + y + z }; // 错误 double型别之和可能无法用int进行表示

而选用小扩号,就不会进行窄化型别转换检查,因为如果那样的话就会破坏太多的遗留代码。

int sum2(x + y + z); // 没问题(表达式的值会被截断为int)
int sum3 = x + y + z; // 同上
  1. 大括号的另个特性,解决C++解析语法:任何能够解析为声明的都要解析为声明。
    这个问题经常在使用默认构造函数,使用对象创建的多线程之类的常见会出现。
    举个例子
Widget w2(); // 我想创建一个名为w2的Widget对象实例,调用默认构造函数
             // 但是这里却认为是声明一个名为w2,返回值为Widget型别的对象函数

std::thread t1(Widget w); // 这里理解为 名为t1, 返回值为std::thread的一个函数 

因为函数声明不能以大括号来指定形参,所以使用大括号就可以解决上述的问题。

Widget w3{}; // 调用没有形参的Widget构造函数
注意大括号与auto使用时

使用大括号初始化,有时候也会出现意外,主要是大括号的初始化物,std::initializer_list以及构造函数重载决议之间的纠结关系。
比如如果使用大括号初始化物来初始化一个使用auto声明的变量,那么进行auto型别推导的结果就是std::initializer_list。当然只要形参中
没有一个具有std::initizlizer_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); // 构造函数的形参中没有任何一个具备std::initializer_list型别
  Widget(int i, double d);
  Widget(std::initializer_list<long double> ld); // 增加带std::initializer_list型别的参数的构造函数
};
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型别

即使是平常会执行的复制或者移动构造函数也有可能被带有std::initializer_list型别形参构造函数劫持:

class Widget {
 public:
  Widget(int i, bool b); // 构造函数的形参中没有任何一个具备std::initializer_list型别
  Widget(int i, double d);
  Widget(std::initializer_list<long double> ld); // 增加带std::initializer_list型别的参数的构造函数
  operator float() const; //强制转换成float型别
};
Widget w5(w4); // 使用小括号,调用的是复制构造函数
Widget w6{w4}; // 调用带有std::initializer_list型别的构造函数, w4的返回值被强制类型转换成float后又被转换成long double。
Widget w7(std::move(w4)); // 使用小括号,调用的是移动构造函数
Widget w8{std::move(w4)}; // 使用大括号,调用的是带有std::initialzer_list的形参构造函数

编译器会优先将大括号初始化匹配到带std::initializer_list型别的参数的构造函数

class Widget {
 public:
  Widget(int i, bool b); 
  Widget(int i, double d);
  Widget(std::initializer_list<bool> ld); 
};
Widget w{10, 5.0}; // 错误! 要求窄化型别转换

编译器会忽略前两个构造函数,转而尝试带有一个std::initializer_list型别的形参构造函数。 而调用该函数要求将一个
int和double的强制转换成bool型别。而在大括号中是禁止窄化型别,所以调用不合法,导致代码编译不通过。

只有找不到任何办法将大括号的初始化物实参转换成std::initializer_list模板中的型别,编译器才会退出去选则普通的重载决议。
比如我们这里将std::initializer_list换成std::initializerstd::string。在上面的例子我们找不任何方法将
int和bool 转换成std::string型别,就会尝试前面的构造函数。

class Widget {
 public:
  Widget(int i, bool b); 
  Widget(int i, double d);
  Widget(std::initializer_list<std::string>); 
};
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(); // 默认构造函数
  Widget(std::initializer_list<int>); // 带有std::initializer_list型别的形参的构造函数
};
Widget w1; // 调用默认构造函数
Widget w2{}; // 调用默认构造函数
Widget w3(); // 变成了函数声明

如果你想表达自己使用std::initializer_list的构造函数,那么可以视同小括号或大括号,用来包含一个大括号来清楚表达你要干什么。

Widget w4({});
Widget w5{{}}; // 这两个都是使用std::initializer_list的构造函数,传入一个空的std::initializer_list的参数。

说到这里可能会想,这叽叽咕咕说了半天开发有啥用呢。
比如std::vector有一个构造函数 vector(size_type size_, T init_value),用来指定有多少个元素,初值都为什么。
当然还有一个常见的带std::initializer_list的构造函数。这时候用大括号还是小括号就有很大的区别。

std::vector<int> v1(10, 20); // 调用了一个形参中没有任何一个具有std::initializer_list型别的构造函数。
                             // 创建包含十个元素的vector,每个元素的值都为20
std::vector<int> v2{10, 20}; // 调用一个含有std::initializer_list型别的构造函数,其中包含两个元素 10, 20

照《Effective Modern C++》这本书说的,现在一般都把std::vector的接口设计视为败笔,应该汲取教训。
当自己构建一组重载构造函数,只有一个或者多个声明为一个具有std::initializer_list的型别的形参的构造函数,
则使用大括号初始化的客户代码都有可能触发,只具有std::initializer_list型别的重载版本。所以说设计函数时,都应该保证
无论使用大括号还是小括号都不会影响重载版本。

当然大括号和小括号的选择在开发模板的时候非常头痛。

创建一个以任意数量的实参来创建一个任意型别的对象,

template<tepename T, typename... Ts> // 创建对象的型别
void doSomeWork(Ts&&... params) {
  // 利用 params创建局部对象T
  T localObject(std::forward<Ts>(params)...); // 采用小括号
  /** or **/
  T localObject{std::forward<Ts>(params)...}; // 采用小括号
}

那么

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

如果使用小括号,就是 10个20;如果是大括号就是10和20,那个正确呢?头大?

本文基本全部来自《Effective Modern C++》
posted @ 2020-12-24 15:55  cyssmile  阅读(432)  评论(0编辑  收藏  举报