【c++】C++11(二)可变参数模板、emplace系列接口、默认的移动构造和移动赋值
hello~ 很高兴见到大家! 这次带来的是C++中关于C++11这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页: 默|笙

文章目录
接上次的博客<C++11(一)>
一、可变参数模板
1. 基本语法以及原理
- C++11里面支持了可变参数模板,因为是模板,所以它会出现在类模板里面和函数模板里面,之前C++的模板参数可以接收任意类型,而可变参数模板意味着可以接收任意个参数。
- 从前模板参数的类型是实例化的时候确定的–>模板参数的类型和个数都是实例化的时候确定的。它们的本质相同。
- 可变数目的参数被称作参数包。参数包有两种:模板参数包和函数参数包。前者表示0到n个模板参数(可以是类型,非类型如整型常量,模板模板参数如模板本身),也就是把接收到的模板参数都打包在一起(打包类型像int,double,string类型等);后者表示0到n个函数参数,函数参数包是把接收到的所有的形参(像x,y变量形参)打包在一起。
- 参数包在概念上可以看作一个编译期的类型列表(对模板参数包)或值列表(对函数参数包),这些模板参数类型和函数参数值被组织在这个列表中。但与数组不同的是,我们不能直接通过下标访问其中的元素,必须通过模板展开机制来逐个处理。
- 我们用省略号…来表示这是一个模板参数包或者是函数参数包。声明模板参数包时,…要写在模板参数包Args名字的前面;用模板参数包Args创建函数参数包时args,…要写在模板参数包名字的后面。之后要使用函数参数包的话,…需要写在函数参数包args名字的后面。这里加…的地方需要记忆。
- 我认为声明的话…是在前面的,使用的话…是在后面的。对于模板参数包Args,声明的时候…在前,对于函数参数包args声明…也是在前面,而这个对于Args来说是使用,所以放在后面。使用args时…自然得放在后面了。args的全称是arguements也就是参数的意思。
template<class... Args> void Func(Args... args) {}
template<class... Args> void Func(Args&... args) {}
template<class... Args> void Func(Args&&... args) {}
- 我们一般会采用第三个万能引用版本,因为它既能匹配左值也能匹配右值,后续也能通过完美转发实现值类别的保持。在进行实例化的时候,…会对模板参数包进行展开,然后根据传过来的实参进行独立的类型推导,最后根据引用折叠规则来确定每一个函数参数的最终类型。
- 在这里我们可以用size_of…运算符去计算参数包中参数的个数。不要忘了size_of…后面的三个.。
- 还有,Args和args只是包的名字,这个不是固定的。
template <class ...Args>
void Print(Args&&... args)//...是放在&&的后面的
{
cout << sizeof...(args) << endl;
}
int main()
{
//结合引用折叠实例化为void Print()
Print();
//实例化为void Print(int&& arg1)
Print(1);//args函数包有一个形参
int x;
//实例化为void Print(int&& arg1, int& arg2)
Print(1, x);//两个
//实例化为void Print(int&& arg1, string&& arg2, int& arg3)
Print(1, string("111"), x);//三个
return 0;
}
- 我们只使用了一个可变参数函数模板,结果却能完成4个函数模板的工作,这是对泛型编程的延伸。在类型的泛型基础上增加了数量的泛型。
2. 包扩展(了解)
- 包扩展就是将这个包分解,然后一次性取出这个列表里被组织的元素。毕竟,一个参数包是一个整体,我们一般用它也是用这一整个参数包。
void ShowList()
{
cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " ";
ShowList(args...);
}
template <class ...Args>
void Print(Args... args)
{
ShowList(args...);
}
int main()
{
int x = 1;
Print(1, string("111"), x);
return 0;
}
- 比如,我们调用Print函数,传递给它三个实参,编译器会将这三个实参打包成一个函数参数包args;之后调用ShowList函数,将args传给ShowList,由于ShowList使用了递归展开的方式处理参数包,它有两个重载版本:一个处理单个参数和剩余参数包,另一个作为递归终止条件处理空包。会先调用前者,也就意味着args里面的第一个参数会传递给x,这样就分离出了参数1,而剩下的传给ShowList函数它的函数参数包args,再分离,如此往复,直到参数包里面的参数都被分解出来,全部分解之后会调用无参的ShowList函数,包扩展完成。
3. emplace系列接口


- C++11以后STL容器新增了emplace系列接口,string不支持,因为string不是模板,而是具体的类型。观察emplace系列接口的定义就能发现,它们全部是可变参数模板。emplace与emplace_back的功能分别跟insert和push系列差不多,不过在此之上做了新的延伸:它的效率比起使用insert和push系列会提高,接下来我来进行解释。


- 在类型为list<mosheg::string>的变量 lt 里存入右值"111",用push_back是构造+移动构造,而用emplace是直接进行构造,这是因为push_back会先通过C风格字符串调用构造函数构造出string类型的临时变量,然后再通过这个临时变量移动构造(因为是右值)到list里面去,所以是构造+移动构造。而用emplace_back的话,它会将"111"打包成一个函数参数包传递给emplace_back函数,然后emplace_back函数通过这个参数包进行实例化,接下来又把这个参数传递下去,直到通过forward<Args>(args)…对参数包进行展开,将参数包里的"111"传递给适合它的构造函数。所以是直接构造。
- 虽然对于右值,emplace_back 避免的移动构造带来的效率提升可能没有那么显著,但对于左值,emplace_back 避免的拷贝构造带来的效率提升是非常明显的。
- ps:这里图片里的list不是库里的,只是用这个list的结构演示过程。

- 但是,emplace系列也无法完全取代push系列,它无法直接使用{}列表初始化,而push_back可以,库里面没有用万能引用(函数模板)实现push_back也是这个原因。因为emplace函数是一个函数模板,它的类型并不是一开始初始化对象就确定了的,而{}列表在没有上下文类型信息时会被推导为std::initializer_list类型,我们通常使用{}列表初始化时,会传递多个参数,当我们使用{}列表初始化传递多个参数时,如果这些参数的类型不一致,就无法形成有效的std::initializer_list,最终导致编译错误,就比如"111"时C风格字符串(const char*),而1是int类型。
二、新的类功能
1. 默认的移动构造和移动赋值
-
在原来的C++类中,是有六个默认成员函数:构造/拷贝构造/析构/赋值重载/取地址重载/const取地址重载。而C++11又增加了两个新的默认成员函数:移动构造函数和移动赋值运算符重载。
-
编译器自动生成默认移动构造和生成移动赋值重载函数的要求比较严格,首先你没有自己实现移动构造和移动赋值重载函数,自己实现了编译器就不会生成了,其次你没有实现析构函数、拷贝构造、赋值重载函数里面的任意一个函数,只要实现了一个就算没有自己实现编译器也不会自动生成了。
-
这个要求其实是非常合理的,毕竟移动构造和移动赋值对于内置类型都是浅拷贝,对于有资源的自定义类型是窃取资源。如果这个类没有资源要释放,一般也不需要实现拷贝构造和赋值重载函数,浅拷贝就已经够用,那么移动构造也是这样。如果有资源需要释放,拷贝构造和赋值重载需要自行实现深拷贝,而移动构造和移动赋值相应也需要实现窃取/交换资源。
class Person
{
public:
Person(const char* name = "张三", int age = 10)
:_name(name)
, _age(age)
{}
/*~Person()
{}*/
private:
mosheng::string _name;
int _age = 1;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
有自主实现的析构函数之前,move(s1)会调用移动构造,而s4会调用移动赋值重载:
实现之后,编译器没有生成默认移动构造和移动赋值重载:
2. default和delete
- 如果要使用的一些默认函数因为某些原因没有生成,我们可以使用default关键字显式指定对应默认函数的生成。
class Person
{
public:
Person(const char* name = "张三", int age = 10)
:_name(name)
, _age(age)
{}
~Person()
{}
Person(const Person& p) = default;
Person(Person&& p) = default;
Person& operator=(const Person& p) = default;
Person& operator=(Person && p) = default;
private:
mosheng::string _name;
int _age = 1;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}

-
我们可以使用 = default 强制要求编译器生成移动构造函数和移动赋值运算符,但前提是这些函数在正常情况下应该是可生成的。如果我们使用了 = default 来显式生成移动操作,但类中已经定义了析构函数,那么编译器可能无法生成有效的默认移动操作,这通常会导致编译错误。为了避免这种情况,通常需要同时使用 = default 来显式生成拷贝构造和赋值重载函数,以确保类的六大特殊成员函数(构造、析构、拷贝构造、拷贝赋值、移动构造、移动赋值)的语义一致性。
-
如果要限制某些默认函数的生成,在C++98中,是将该函数设置成private,并且只声明不定义,这样其他人若想使用这个默认函数就会报错。而在C++里更加简单,只需要在该函数声明加上 =delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
3. 委托构造(了解)
- C++中的委托构造函数是C++11引入的特性,它允许一个构造函数调用同类中的其他构造函数,实现复用。
- 被委托的构造函数必须初始化所有成员变量,也就是走一遍初始化列表,因为委托构造函数后不能再重复初始化,如果重复初始化的话会编译报错。
class Example {
public:
Example(int a, int b)
:_x(a)
,_y(b)
{
cout << "目标构造函数\n";
}
//Example(int a)
// : Example(a, 0)
// ,_y(1) // 编译报错,因为_y已经在被委托的构造函数里面初始化了
//{
// cout << "委托构造函数\n";
//}
Example(int a)
: Example(a, 0)
{
cout << "委托构造函数\n";
}
int _x;
int _y;
};
4. 继承构造(了解)
-
继承构造函数是C++11引入的一项特性,它允许派生类直接继承基类的构造函数,而不需要手动重新定义它们。
-
这一特性显著简化了派生类的编写,特别是在基类有多个构造函数的情况下。
派生类继承基类的普通构造函数,特殊的拷贝构造函数/移动构造函数不继承。 -
继承构造函数中派生类自己的成员变量如果有缺省值会使用缺省值初始化,如果没有缺省值那么跟之前类似,
-
内置类型成员不确定,具体看编译器的处理,自定义类型成员调用用默认构造初始化。
-
看一遍知道有这个东西就行了。
class Base {
public:
Base(int x, double d)
:_x(x)
, _d(d)
{
}
Base(int x)
:_x(x)
{
}
Base(double d)
:_x(d)
{
}
protected:
int _x = 0;
double _d = 0;
};
//传统的派生类实现构造
class Derived : public Base {
public:
Derived(int x) : Base(x) {}
Derived(double d) : Base(d) {}
Derived(int x, double d) : Base(x, d) {}
};
//C++11继承基类的所有构造函数
//1、没有成员变量的派生类
//2、成员变量都有缺省值,并且我们就想用这个缺省值初始化
class Derived : public Base {
public:
using Base::Base;
protected:
int _i;
string _s;
};
今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!
更多推荐
所有评论(0)