【C++】C++11新特性(中)
目录

个人主页:矢望
博客专栏:C++专栏、Linux专栏、C语言专栏、数据结构专栏
一、C++11
1.1 右值引用 和 移动语义
1.1.1 完美转发
在上期博客我们实现了万能引用,并做了举例,今天我们要解决万能引用带来的参数转发时值类别丢失的问题。
// 万能引用
template<class T>
void Function(T&& t)
{
Fun(t);
}
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
如上图,我要在万能引用函数中进行参数转发,也就是要调用Fun系列函数。
主函数代码:
int main()
{
// 模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// 模板实例化为void Function(int& t)
Function(a); // 左值
// 模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// 模板实例化为void Function(const int& t)
Function(b);
// 模板实例化为void Function(const int&& t)
Function(std::move(b)); // const 右值
return 0;
}
运行结果:
从上面的结果中,我们可以明显感觉到值类别丢失的问题。在上面的万能引用中传左值会显示左值引用,传右值会显示右值引用,这没有问题,但是,无论传递的是左值还是右值,万能引用函数中变量表达式的属性都是左值。因此,再次传递Fun函数,原来的值类别就丢失了,都成了左值引用。
此时的参数传递就需要用到完美转发了。
C++11设计完美转发,就是为了解决在泛型编程中参数转发时丢失值类别信息这一核心痛点。
std::forward在模板函数中转发参数时,会保留其原始的左值/右值属性,以调用最匹配的重载函数,实现最高效率。
// 万能引用
template<class T>
void Function(T&& t)
{
//Fun(t);
// 完美转发
Fun(forward<T>(t));
}

完美转发forward本质是一个函数模板,它主要还是通过引用折叠的方式实现。在上面的示例中,传递的实参是右值时,T会被推导为非引用类型,如int,在forward内部t会被强转为右值引用返回;传递的实参时左值时,T会被推导为左值引用类型,如int&,在forward内部t会被强转为左值引用返回。
std::forward本身不做类型推导,它只是利用模板参数T中已经推导好的类型信息,通过引用折叠规则来恢复参数原始的值类别。
这就是为什么必须写成std::forward<T>(t)而不是std::forward(t)——因为T包含了转发所需的所有类型信息,没有这个信息,转发就无法"完美"进行。
关于 move 和 forward
在上述场景中如果强制使用move进行转发,会造成很严重的一系列问题,如:左值资源被意外移动,导致空指针、悬空引用等。
std::move意味着我明确要放弃这个对象;std::forward意味着请保持这个参数的原始意图。
在泛型编程中,我们通常不知道调用者的意图,所以应该使用std::forward来忠实地传递参数。只有在明确知道要转移所有权时,才使用std::move。
1.2 可变参数模板
可变参数的概念源自于C语言:
C++11在此基础上延展出了可变参数模板。
1.2.1 基本语法及其原理
C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}
这里的Args的名称不是固定的,由自己定义,注意这里...的位置的语法规则。
我们用省略号...来指出一个模板参数或函数参数的一个参数包,在模板参数列表中,class...或typename...指出接下来的参数表示零或多个类型列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
简单认识:
template <class ...Args>
void Print(Args... args)
{
// 使用运算符 sizeof... 计算
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print(); // 包⾥有0个参数
Print(1); // 包⾥有1个参数
Print(1, string("xxxxx")); // 包⾥有2个参数
Print(1.1, string("xxxxx"), x); // 包⾥有3个参数
return 0;
}

原理1:编译本质这里会结合引用折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
原理2:更本质去看没有可变参数模板,我们需要实现出这样的多个函数模板才能支持这里的功能,有了可变参数模板,我们进一步被解放,它是类型泛化基础上叠加数量变化,让我们泛型编程更灵活。
void Print();
template <class T1>
void Print(T1&& arg1);
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
1.2.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()
{
double x = 2.2;
Print(); // 包⾥有0个参数
Print(1); // 包⾥有1个参数
Print(1, string("hello world")); // 包⾥有2个参数
Print(1.1, string("hello world"), x); // 包⾥有3个参数
return 0;
}
解释:递归展开机制:每次取出参数包的第一个参数x,剩余包为args...,再次递归调用,此时参数包大小减1,当包大小为0时,模板版本ShowList(T x, Args... args)无法匹配空参数,所以还需要无参数的ShowList()终止函数来处理空参数包的情况。
程序运行:
底层实现细节:
注意:推导方式是编译时推导。
1.2.3 emplace 系列接口
C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为可变模板参数,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container<T>,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。

emplace_back的工作机制:原地构造,在节点的data成员位置直接调用构造函数;完美转发,将参数原样转发给构造函数。
传递参数包过程中,如果是 Args&&... args 的参数包,要用完美转发参数包,方式如下forward<Args>(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左值。
emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列。
对于浅拷贝类型,emplace系列接口在性能和安全型上都优于push系列:
std::vector<MyClass> vec;
// push系列:在调用处构造临时对象,然后拷贝/移动到容器中
vec.push_back(MyClass(10)); // 构造临时对象 + 移动
// emplace系列:在容器内部直接构造,避免临时对象
vec.emplace_back(10); // 直接在vector中构造
emplace_back可以避免不必要的拷贝,消除共享资源的风险例如共享指针问题,性能更优。push_back传递现有对象时,会触发浅拷贝,可能导致资源共享,可能产生悬空指针。
对于深拷贝类型,emplace_back在性能上明显优于push_back,因为它避免了不必要的拷贝或移动操作:
// 场景1:push_back现有对象
std::string str = "这是一个很长的字符串,会触发深拷贝";
std::vector<std::string> vec;
vec.push_back(str); // 深拷贝发生!
// 场景2:push_back临时对象
std::vector<std::string> vec;
vec.push_back(std::string("临时字符串")); // 构造+移动
// 场景3:emplace_back
std::vector<std::string> vec;
vec.emplace_back("直接构造的字符串"); // 直接构造,无拷贝
push_back需要先有对象,要么拷贝现有对象,要么构造临时对象;emplace_back直接构造,在容器内部一次性完成构造,避免中间步骤。
总结:emplace系列是push/insert系列的现代替代品,在几乎所有场景下都提供更好或相等的性能,是现代C++编程的首选。
1.3 新的类功能
1.3.1 默认的移动构造和移动赋值
原来C++类中,有6个默认成员函数:构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const 取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果自己没有实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。也就是说深拷贝类型,需要自己实现移动构造,因为深拷贝类型,析构、拷贝构造等等这些都是必要的。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
默认移动赋值跟上面移动构造完全类似。
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
1.3.2 委托构造函数
1、C++11引入了委托构造函数的特性,允许一个构造函数调用同类中的其它构造函数,从而减少代码重复并提高可维护性。
例如:
class Example {
public:
Example(int a, int b)
:_x(a)
,_y(b)
{
cout << "目标构造函数,被委托的函数\n";
}
Example(int a)
: Example(a, 0)
{
cout << "委托构造函数\n";
}
int _x;
int _y;
};
int main()
{
Example(1, 2);
Example(1);
return 0;
}
2、被委托的构造函数(被其它构造函数调用的构造函数),必须初始化所有成员变量,因为委托构造函数后,不能再重复初始化。
Example(int a)
: Example(a, 0)
,_y(1) // 不能
{
cout << "委托构造函数\n";
}
如果是上面这样写会导致编译错误,因为_y重复初始化了。
1.3.3 继承构造函数
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:
using Base::Base;
protected:
int _i;
string _s;
};
本质上相当于:
class Derived : public Base
{
public:
Derived(int x) : Base(x) {}
Derived(double d) : Base(d) {}
Derived(int x, double d) : Base(x, d) {}
protected:
int _i;
string _s;
};
本质是让编译器自己生成派生类对应基类参数的构造,这就是继承基类的所有构造函数,适用于:1、没有成员变量的派生类。2、成员变量都有缺省值,并且就想用这个缺省值初始化。
1.3.4 default 和 delete
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
Person(Person&& p) = default;
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁而已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上= delete即可,该语法指示编译器不生成对应函数的默认版本,称= delete修饰的函数为删除函数。
Person(const Person& p) = delete;
1.3.5 final 和 override
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~
更多推荐

所有评论(0)