学习一下关于C++基础知识大括号。

0一个例子

 1#include <iostream>
 2using namespace std;
 3struct A
 4{
 5    A(int x);
 6};
 7
 8A::A(int x)
 9{
10    cout << "A::A x = " << x << endl;
11}
12
13int main() {
14    A a(1);
15    A b{2};
16    return 0;
17}

输出结果

1A::A x = 1
2A::A x = 2

很显然,在本文的演示demo中,构造函数使用大括号和小括号两种方式是相同的作用。那么,是不是所有的情况大括号都可以使用呢?

再进一步研究

 1#include <iostream>
 2using namespace std;
 3struct A
 4{
 5    ~A(){ cout << "~A()" << endl; }
 6    A(int x)
 7    {
 8        cout << "A::A x = " << x << endl;
 9    }
10};
11
12
13struct B
14{
15    A a;
16    //B(){ cout << "B::B()" << endl;}
17    ~B(){ cout << "~B()" << endl; }
18};
19
20int main() {
21    B b1(1);//报错
22    B b2{1};//没问题
23    return 0;
24}

可以发现括号和大括号是有区别的,如果去掉报错输出

1A::A x = 1
2~B()
3~A()

这里发现大括号有初始化类内的元素的作用,属于聚合初始化,相当于B b2={1}等价成b2.a = 1,然后隐式的对1进行构造为b2.a = A(1)。这里的A(1)是一种c++的匿名构造。关于匿名构造可以点击这里。 因此可以调用单参数构造函数A::A(int)对b2.a进行初始化。

 1//1.会进行一次A的匿名构造,a=1会隐式转化成A(1)
 2A(int x)
 3{
 4	cout << "A::A x = " << x << endl;
 5}
 6//2.然后在进行右值运算,调用默认拷贝构造函数
 7//B(const B &)(B)或者是B(B &&)(B)
 8B(const B& b1)
 9{
10	this->a = b1->a;
11}
12//相当于是b2.a=A(1)只会调用一次A的构造,然后会对B进行默认拷贝构造

本文所有demo,笔者在Clion中演示结果。

1大括号

C++11标准开始就引入了列表初始化的概念,即支持使用{}对变量或对象进行初始化,且与传统的变量初始化的规则一样,也分为拷贝初始化和直接初始化两种方式。大括号的本质实际上是std::initializer_list,直接讲解感觉会感觉云里雾里。

Q:首先看一下下面的初始化,哪一种是正确的?

1int x(0);    // 初始值在圆括号内
2
3int y = 0;   // 初始值在等号后面
4
5int z{0};    // 初始值在大括号内

A:实际上,三种初始化都是正确的。只是三种初始化的含义不同的,需要区别对待。

  1. 第一种,使用括号的方式,对变量或对象使用括号初始化的方式被称为直接初始化,其本质是调用了相应的构造函数
  2. 第二种,使用等号的方式,用等号初始化的方式则被称为拷贝初始化
  3. 第三种,使用大括号的方式,除非存在接受std::initializer_list的构造函数,否则使用大括号构造对象等效于使用括号。

初始化和赋值操作的差别是模糊的。但是对于用户定义的类,区分初始化和赋值操作是很重要的,因为这会导致不同的函数调用。

 1//以下面的demo为例
 2#include <iostream>
 3using namespace std;
 4struct A
 5{
 6    ~A(){ cout << "~A()" << endl; }
 7    A()
 8    {
 9        cout << "A::A " << endl;
10    }
11
12    A(A& a)
13    {
14        cout << "A::A copy constructor" << endl;
15    }
16
17    A& operator=(A &a)
18    {
19        cout << "A::A operator=" << endl;
20        return *this;
21    }
22};
23
24int main() {
25    A a1;			//初始化,对应无参构造
26    A a2 = a1;		//初始化,对应拷贝构造
27    a1 = a2;		//赋值重载运算,对应operator=操作
28    return 0;
29}

1.1大括号类内初始化

1class A {
2  ...
3private:
4  int x{ 0 };   // x的默认初始值为0
5  int y = 0;    // 同上
6  int z( 0 );   // 报错
7}

大括号也可以用于类内成员的默认初始值,在C++11中,等号”=”也可以实现,但是圆括号 ‘( )’ 则不可以

1.2大括号初始化不可拷贝对象

1std::atomic<int> ai1{ 0 };  // 可以
2
3std::atomic<int> ai2( 0 );  // 可以
4
5std::atomic<int> ai3 = 0;   // 报错

不可拷贝对象(例如,std::atomic)可以用大括号圆括号初始化,但不能用等号

1.3免疫C++中的最让人头痛的歧义

 1#include <iostream>
 2using namespace std;
 3struct A
 4{
 5    ~A(){ cout << "~A()" << endl; }
 6    A()
 7    {
 8        cout << "A::A " << endl;
 9    }
10    
11    A(int x)
12    {
13        cout << "A::A x = " << x << endl;
14    }
15
16    A(A& a)
17    {
18        cout << "A::A copy constructor" << endl;
19    }
20
21    A& operator=(A &a)
22    {
23        cout << "A::A operator=" << endl;
24        return *this;
25    }
26};
27
28int main() {
29    A a1(10);  // 调用的A带参构造函数,这个没有歧义
30    A a2();   // 最让人头痛的歧义,声明了一个名为a2,不接受任何参数,返回A类型的函数!
31	A a3;     // 正确:a3是个默认初始化的对象
32    return 0;
33}

这里产生歧义的点,不知道a2为无参数返回为A类型的函数名,还是为A的默认初始化对象实例。

使用下面大括号的方式,可以避免歧义产生,使用大括号包含参数是无法声明为函数的

1A a4{};   // 无歧义

1.4迭代器的使用

 1#include<iostream>
 2#include<vector>
 3#include<map>
 4 
 5class Test
 6{
 7public:
 8    Test(std::string s, int val) {}
 9};
10 
11 
12int main()
13{
14    int arr[] = {10, 20, 30, 40};  //拷贝初始化
15    int brr[]{10, 20, 30, 40};    //直接初始化
16 
17    std::vector<int> vc1 = {10, 20, 30 ,40};
18    std::vector<int> vc2{10, 20, 30, 40};
19    std::map<std::string, int> m1 = { {"a", 1}, {"b", 2}, {"c", 3} };
20 
21    Test *pt = new Test{"test", 100};
22    delete pt;
23 
24    return 0;
25}

上面所举的例子中用到了{}对标准库容器进行初始化,而标准库容器之所以能够支持初始化列表,除了有编译器的支持外,更直接的是这些容器存在以std::initializer_list为形参的构造函数

vector容器初始化

il_1

map容器初始化

il_2

1.5与std::initializer_list混淆

1.5.1构造中没有添加initializer_list

 1#include<iostream>
 2using namespace std;
 3
 4class A {
 5public:
 6  A(int i, bool b){
 7      cout << "constructor int/bool" << endl;
 8  }
 9  A(int i, double d){
10      cout << "constructor int/double" << endl;
11  }
12
13};
14
15int main()
16{
17    // 调用第一个构造函数
18    A a1(10, true);
19    // 调用第一个构造函数,使用大括号构造对象等效于使用括号
20    A a2{10, true};
21    // 调用第二个构造函数
22    A a3(10, 5.0);
23    // 调用第二个构造函数,使用大括号构造对象等效于使用括号
24    A a4{10, 5.0};
25    return 0;
26}

输出

1constructor int/bool
2constructor int/bool
3constructor int/double
4constructor int/double

1.5.2构造中添加initializer_list

如果构造函数的形参带有std::initializer_list,调用构造函数时大括号初始化语法会强制使用std::initializer_list参数的重载构造函

 1#include<iostream>
 2using namespace std;
 3
 4class A {
 5public:
 6  A(int i, bool b){
 7      cout << "constructor int/bool" << endl;
 8  }
 9  A(int i, double d){
10      cout << "constructor int/double" << endl;
11  }
12
13  A(std::initializer_list<long double> il){
14      cout << "constructor initializer_list size = " << il.size() << endl;
15      //使用迭代器输出
16      for (const long double* i = il.begin();  i != il.end(); ++i)
17      {
18            cout << *i << endl;
19      }
20  }
21};
22
23int main()
24{
25    // 调用第一个构造函数
26    A a1(10, true);
27    // 调用第三个构造函数,里面参数强制转化为long double类型
28    A a2{10, true};
29    // 调用第二个构造函数
30    A a3(10, 5.0);
31    // 调用第三个构造函数,里面参数强制转化为long double类型
32    A a4{10, 5.0};
33    return 0;
34}

输出

1constructor int/bool
2constructor initializer_list size = 2
310
41
5constructor int/double
6constructor initializer_list size = 2
710
85

1.5.3initializer_list类的类型转化

更进一步的,编译器用带有std::initializer_list构造函数匹配大括号初始值,即使这个构造函数是无法调用的且另一个构造函数还是参数精确匹配的,编译器也会忽略精准匹配的构造函数。

 1#include<iostream>
 2using namespace std;
 3
 4class A {
 5public:
 6    A(int i, bool b){
 7      cout << "constructor int/bool" << endl;
 8    }
 9    A(int i, double d){
10      cout << "constructor int/double" << endl;
11    }
12
13    A(std::initializer_list<long double> il){
14      cout << "constructor initializer_list size = " << il.size() << endl;
15      for (const long double* i = il.begin();  i != il.end(); ++i)
16      {
17            cout << *i << endl;
18      }
19    }
20
21    operator float() const   // 支持隐式转换为float类型
22    {
23      float f = 1.0f;
24      return f;
25    }
26
27    A(const A& a)
28    {
29      cout << "A::A copy constructor" << endl;
30
31    }
32};
33
34int main()
35{
36    // 调用第一个构造函数
37    A a1(10, true);
38    // 不使用拷贝构造,将a1隐式转化为float类型,在使用第三个构造
39    A a2{a1};
40    // 调用拷贝构造
41    A a3(a1);
42    return 0;
43}

输出

1constructor int/bool
2constructor initializer_list size = 1
31
4A::A copy constructor

1.5.4initializer_list窄化转换

根据1.5.2的内容,稍作修改把构造参数initializer_list中的类型long double变成bool类型

 1#include<iostream>
 2using namespace std;
 3
 4class A {
 5public:
 6    A(int i, bool b){
 7        cout << "constructor int/bool" << endl;
 8    }
 9    A(int i, double d){
10        cout << "constructor int/double" << endl;
11    }
12
13    A(std::initializer_list<bool> il){
14        cout << "constructor initializer_list size = " << il.size() << endl;
15        //使用迭代器输出
16        for (auto* i = il.begin();  i != il.end(); ++i)
17        {
18            cout << *i << endl;
19        }
20    }
21};
22
23int main()
24{
25    // 调用第一个构造函数
26    A a1(10, true);
27    // 调用第三个构造函数,里面参数强制转化为bool类型,但是int类型转化bool会错误
28    A a2{10, true};//无法编译通过
29    // 调用第二个构造函数
30    A a3(10, 5.0);
31    // 调用第三个构造函数,里面参数强制转化为bool类型,但是int、double类型转化bool会错误
32    A a4{10, 5.0};//无法编译通过
33    return 0;
34}

输出

1D:/project/CPPProject0710/main.cpp:28:18: error: narrowing conversion of '10' from 'int' to 'bool' [-Wnarrowing]
2   28 |     A a2{10, true};
3      |                  ^
4D:/project/CPPProject0710/main.cpp:32:17: error: narrowing conversion of '10' from 'int' to 'bool' [-Wnarrowing]
5   32 |     A a4{10, 5.0};
6      |                 ^
7ninja: build stopped: subcommand failed.

1.5.5 无法转化为initializer_list

只有当大括号内的值无法转换std::initializer_list元素的类型时,编译器才会使用正常的重载选择方法

根据1.5.2的内容,稍作修改把构造参数initializer_list中的类型long double变成string类型

 1#include<iostream>
 2using namespace std;
 3
 4class A {
 5public:
 6    A(int i, bool b){
 7        cout << "constructor int/bool" << endl;
 8    }
 9    A(int i, double d){
10        cout << "constructor int/double" << endl;
11    }
12
13    A(std::initializer_list<string> il){
14        cout << "constructor initializer_list size = " << il.size() << endl;
15        //使用迭代器输出
16        for (auto* i = il.begin();  i != il.end(); ++i)
17        {
18            cout << *i << endl;
19        }
20    }
21};
22
23int main()
24{
25    // 调用第一个构造函数
26    A a1(10, true);
27    // 调用第三个构造函数,里面参数强制转化为string类型,类型无法转化匹配,调用第一个构造函数
28    A a2{10, true};
29    // 调用第二个构造函数
30    A a3(10, 5.0);
31    // 调用第三个构造函数,里面参数强制转化为string类型,类型无法转化匹配,调用第一个构造函数
32    A a4{10, 5.0};
33    return 0;
34}

输出

1constructor int/bool
2constructor int/bool
3constructor int/double
4constructor int/double

1.5.6关于混淆点补充

在STL中,许多容器都会使用到std::initializer_list构造函数来初始化,比如vector和map等。std::vector就是一个被它们直接影响的类std::vector中有一个可以指定容器的大小和容器内元素的初始值的不带std::initializer_list构造函数,但它也有一个可以指定容器中元素值的std::initializer_list函数

1std::vector<int> v1(10, 20);   // 使用不带std::initializer_list的构造函数
2                               // 创建10个元素的vector,每个元素的初始值为20
3
4std::vector<int> v2{10, 20};   // 使用带std::initializer_list的构造函数
5                               // 创建2个元素的vector,元素值为10和20

上述调用走的不是同一个构造函数,即含义完全不同

 1//第一个小括号构造吗,创建10个元素的vector,每个元素的初始值为20
 2template<typename _InputIterator,
 3typename = std::_RequireInputIter<_InputIterator>>
 4    vector(_InputIterator __first, _InputIterator __last,
 5           const allocator_type& __a = allocator_type())
 6    : _Base(__a)
 7    {
 8        _M_range_initialize(__first, __last,
 9                            std::__iterator_category(__first));
10    }
11//第二个大括号构造,创建2个元素的vector,元素值为10和20
12vector(initializer_list<value_type> __l,
13       const allocator_type& __a = allocator_type())
14    : _Base(__a)
15    {
16        _M_range_initialize(__l.begin(), __l.end(),
17                            random_access_iterator_tag());
18    }

另外使用一个大括号调用initializer_list无参构造的方式

默认使用大括号的无参构造

如果你想要用一个空的std::initializer_list参数来调用带std::initializer_list构造函数,那么你需要把大括号作为参数,即把空的大括号放在圆括号内或者大括号内,即initializer_list参数列表可以为空。

 1#include<iostream>
 2using namespace std;
 3
 4class A {
 5public:
 6    A(){
 7        cout << "constructor" << endl;
 8    }
 9
10
11    A(std::initializer_list<int> il){
12        cout << "constructor initializer_list size = " << il.size() << endl;
13        //使用迭代器输出
14        for (auto* i = il.begin();  i != il.end(); ++i)
15        {
16            cout << *i << endl;
17        }
18    }
19};
20
21int main()
22{
23    //调用默认构造
24    A a1;
25    //调用默认构造
26    A a2{};
27    //调用initializer_list构造
28    A a3({});
29    //调用initializer_list构造,里面又是一个参数,为0
30    A a4{{}};
31    return 0;
32}

输出结果

1constructor
2constructor
3constructor initializer_list size = 0
4constructor initializer_list size = 1
50

可以发现有时候{}和()可以是同样的效果(上述a1和a2),又可以是不一样的效果(上述a3和a4)。

2最后来一个demo

 1#include<iostream>
 2#include<thread>
 3using namespace std;
 4
 5struct A
 6{
 7    ~A(){ cout << "~A()" << endl; }
 8    A()
 9    {
10        cout << "A::A " << endl;
11    }
12	//这里必须写成右值引用的形式,std::thread构造传入的值为右值,会进行一次拷贝构造
13    A(const A& a)
14    {
15        cout << "A::A copy constructor" << endl;
16
17    }
18
19    A& operator=(A &a)
20    {
21        cout << "A::A operator=" << endl;
22        return *this;
23    }
24    //仿函数
25    void operator()()
26    {
27        cout << "A::operator() " << endl;
28        //return true;
29    }
30};
31
32void f1(int n)
33{
34    cout << "Thread f1 " << n << " executing\n";
35}
36
37int main()
38{
39    thread t1{};		//空的默认构造
40    thread t2;			//空的默认构造,不能写成thread t2();会被认为是函数定义
41    thread t3{f1, 1};	//使用函数指针,最终调用函数指针f1()回调到对应函数
42    t3.join();
43    thread t4(f1, 2);	//使用函数指针,最终调用函数指针f1()回调到对应函数
44    t4.join();
45    
46   	thread t5{A()};		//使用仿函数,A()是匿名对象,传入thread中会调用拷贝构造
47    t5.join();			//这里会释放上面的匿名对象,然后在传入的拷贝对象调用 "对象()",这就是仿函数
48    thread t6{A{}};		//同t5一致
49    t6.join();
50    A a;				//与上述不一致的是,这里调用的a是直接的对象
51    thread t7(a);		//将直接对象传入t7中,传入thread中会调用拷贝构造
52    t7.join();			//然后在传入的拷贝对象调用 "对象()",这就是仿函数,因为a是局部变量,所以不会马上退出
53    thread t8((A()));	//这里跟t5一致
54    t8.join();
55    thread t9(A{});		//这里跟t5一致
56    t9.join();
57    //parentheses were disambiguated as a function declaration [-Wvexing-parse]
58    /*thread t10(A());	//t9会出错,需要变成t8那样才行,这里最外层使用括号,会有歧义
59    t10.join();*/		//thread t9(A());会被认为是函数定义
60    return 0;
61}

关于std::thread

1thread(_Callable&& __f, _Args&&... __args);

当我们做并发工作时,需要使用std::thread()来用于创建、管理和同步线程。首先,我们要知道std::thread()的参数列表构成

第一个参数:可作为线程函数的三种形式

  1. 函数指针

  2. 函数对象(operator()()重载函数,也叫仿函数)

  3. Lambda匿名表达式

其他参数:相当于可变参数列表,根据调用函数的参数列表来传入实参

输出结果,这里一个一个输出

 1//t3输出
 2Thread f1 1 executing
 3//t4输出
 4Thread f1 2 executing
 5//t5输出
 6A::A
 7A::A copy constructor
 8~A()
 9A::operator()
10~A()
11//t6输出
12A::A
13A::A copy constructor
14~A()
15A::operator()
16~A()
17//t7输出
18A::A
19A::A copy constructor
20A::operator()
21~A()
22~A()
23//t8输出
24A::A
25A::A copy constructor
26~A()
27A::operator()
28~A()
29//t9输出
30A::A
31A::A copy constructor
32~A()
33A::operator()
34~A()

参考文献

[1] emin. C++创建对象时区分圆括号( )和大括号{ }, 2020.

[2] 留恋单行路,现代C++之std::initializer_list的特性分析,2022.

[3] zhihu,c++11小括号和大括号初始化有什么区别?,2021.