【C++详解】C++11(三) 可变参数模板、包扩展、empalce系列接⼝、新的类功能
文章目录
- 一、可变参数模板
- 基本语法及原理
- 包扩展
- empalce系列接⼝
- 插入单参数
- 插入多参数
- 实现list的emplace系列接口
- 二、新的类功能
- 默认的移动构造和移动赋值
- 成员变量声明时给缺省值
- default和delete
- final与override
一、可变参数模板
基本语法及原理
- C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函数参数。
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {} //左值引用
template <class ...Args> void Func(Args&&... args) {} //万能引用
- 我们⽤省略号来指出⼀个模板参数或函数参数的⼀个包,在模板参数列表中,class…或typename…指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟…指出接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则。
- 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数或多个类。
- 这⾥我们可以使⽤sizeof…运算符去计算参数包中参数的个数。
template <class... Args>
void Print(Args&&... args)
{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);
// ...
包扩展
- 对于⼀个参数包,我们无法直接获取包内元素的各种属性,除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(…)来触发扩展操作。底层的实现细节如下图所⽰。
- C++还⽀持更复杂的包扩展,直接将参数包依次递归展开作为实参给⼀个函数去处理,并且因为参数包的递归展开依赖编译期的重载匹配 (所以这里的递归和我们之前学习的递归不一样,之前我们接触的都是运行时递归,特点就是自己调自己,而这里严格意义上来说并不是自己调自己),所以递归结束条件不能直接写一个类似
if(sizeof…(args) == 0) return;
的判断在函数体内,因为判断相等是运行时逻辑。应该实现一个空参数的函数重载作为递归结束标志。
void ShowList()
{// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数cout << endl;
}template <class T, class ...Args>
void ShowList(T x, Args... args)
{//递归结束条件无法写成下面这样,包扩展的递归是在编译时递归,// 而下面判断相等只能运行时判断//if (sizeof...(args) == 0) // return;cout << x << " ";// args是N个参数的参数包// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包ShowList(args...);
}// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{ShowList(args...);
}int main()
{Print();Print(1);Print(1, string("xxxxx"));Print(1, string("xxxxx"), 2.2);return 0;
}// Print(1, string("xxxxx"), 2.2);调⽤时
// 本质编译器将可变参数模板通过模式的包扩展,编译器推导的以下三个重载函数函数
//
//void ShowList(double x)
//{
// cout << x << " ";
// ShowList();
//}
//
//void ShowList(string x, double z)
//{
// cout << x << " ";
// ShowList(z);
//}
//
//void ShowList(int x, string y, double z)
//{
// cout << x << " ";
// ShowList(y, z);
//}
//
//void Print(int x, string y, double z)
//{
// ShowList(x, y, z);
//}
包扩展模式展开参数包还有第二种方式,不过这种方式更抽象,而且不需要掌握,实践中不会这样用,小编这里就顺带提一嘴:
template <class T>
const T& GetArg(const T& x)
{cout << x << " ";return x;
}template <class ...Args>
void Arguments(Args... args)
{}template <class ...Args>
void Print(Args... args)
{// 注意GetArg必须返回对象,这样才能组成参数包给Arguments//参数包的每个参数都传给GetArg处理,处理以后返回值作为实参组成参数包传给ArgumentsArguments(GetArg(args)...);cout << endl;
}// 本质可以理解为编译器编译时,包的扩展模式
// 将上⾯的函数模板扩展实例化为下⾯的函数
//void Print(int x, string y, double z)
//{
// Arguments(GetArg(x), GetArg(y), GetArg(z));
//}int main()
{Print();Print(1);Print(1, string("xxxxx"));Print(1, string("xxxxx"), 2.2);return 0;
}
empalce系列接⼝
- C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为
container< T >,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。- emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列。
- 第⼆个程序中我们模拟实现了list的emplace和emplace_back接⼝,这⾥把参数包不断往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
- 传递参数包过程中,如果是 Args&&… args 的参数包,要⽤完美转发参数包,⽅式如下std::forward< Args >(args)… ,否则编译时包扩展后右值引⽤变量表达式就变成了左值。
插入单参数
我们先来分析一个插入需要类型转化的单参数的例子:
list<wusaqi::string> lt;//构造临时对象(隐式类型转换)+移动构造
lt.push_back("111111111111"); //匹配push_back右值引用版本
cout << "*********************************" << endl;
//构造
lt.emplace_back("111111111111"); //将万能引用模板参数推导为const char*&,最后引用折叠成const char*&
cout << "*********************************" << endl;
首先小编在这里科普一下字符串字面量 “111111111111” 的原始类型是 const char[13](12个字符 + 终止符\0),作为函数参数传递时,它会退化为 const char*类型(指针),它实质是左值,下面有个表格大家可以看一下,结论是不是所有字面量都是有值,字符串字面量是左值。
这里push_back底层调用string的构造+移动构造,emplace_back只调用string的构造。原因在于push_back的Value_type(容器成员类型)在list< string >对象创建时就确定为string了,而emplace_back是实参推演模板形参,因为字符串字面量"111111111111"是const char* 类型的左值,所以emplace_back的形参Args(可变模板参数)会被推导为const char*&,然后引用折叠最后折叠成右值引用。push_back因为形参类型和实参类型不匹配,传递参数过程中会发生隐式类型转换,产生临时对象,所以首先会调用一次构造函数构造string类型的临时对象,然后这个通过将这个临时对象当作参数一直往下传递到list_node的构造初始化,然后调用string的移动构造构造出list容器内的string对象。而emplace_back因为传递参数时形参和实参始终是匹配的,所以"111111111111"对象会一直传递到list_node的构造初始化,在容器内部直接构造出string对象。
所以在有类型转换时emplace_back比push_back更高效。
下面是其他单参数的例子,插入单纯的左值或者右值emplace_back和push_back效率相同。
// 传左值,跟push_back⼀样,⾛构造(对象s1)+emplace_back的拷⻉构造
wusaqi::string s1("111111111111");
lt.emplace_back(s1);
cout << "*********************************" << endl;// 传右值,跟push_back⼀样,⾛移动构造
lt.emplace_back(move(s1));
cout << "*********************************" << endl;
插入多参数
list<pair<wusaqi::string, int>> lt1;// 跟push_back⼀样// 构造pair + 拷⻉/移动构造pair到list的节点中data上pair<wusaqi::string, int> kv("苹果", 1);lt1.push_back(kv);lt1.emplace_back(kv);cout << "*********************************" << endl;// 跟push_back⼀样//lt1.push_back(move(kv));lt1.emplace_back(move(kv));cout << "*********************************" << endl;
多参数和单参数一样,插入单纯的左值或者右值emplace_back和push_back效率相同,这里要注意我上面代码emplace_back和push_back都写了一份,在插入move(kv)时kv只能被move一次,所以在运行时要把其中一个move(kv)注释掉。
下面小编来讲解当插入两个独立参数而不是一个pair对象时emplace_back和push_back的区别。原理和前面差不多,push_back因为有多参数隐式类型转换,所以会调用string的构造和移动构造,emplace_back是将两个参数一直传递,直到最后用两个参数直接调用pair的构造函数的同时调用string的构造在list内部构造出一个pair对象,所以只会调用string的构造。
这里有一个细节需要注意,当调用push_back时必须加花括号,因为要插入的参数要匹配已经实例化后的list<pair<wusaqi::string, int>>里的成员变量参数:pair<wusaqi::string,
int>,这里插入的是两个独立参数所以要走多参数隐式类型转换,而走多参数隐式类型转换必须加花括号。当调用emplace_back时又绝对不能加花括号,因为参数会一直原封不动的往下传递给到pair的构造函数,而pair的构造函数需要两个参数,如果一开始就把两个参数用花括号括起来相当于传递的一个initializer_list类型的参数,但是pair<string, int>的构造函数不接受initializer_list作为参数,那么pair的构造函数就无法读取正确的参数形式导致报错。
lt1.push_back({ "苹果", 1 });
//lt1.emplace_back({ "苹果", 1 }); //编译不通过
lt1.emplace_back("苹果", 1);
cout << "*********************************" << endl;
实现list的emplace系列接口
首先实现一个包含万能引用和可变模板参数的emplace_back,注意emplace_back内部不能去调用insert了,因为emplace_back的第二个参数是参数包,和insert不匹配,而应该重新实现一个和insert逻辑一样的emplace(名字随便,小编这里起名为emplace),把第二哥参数改为参数包,最后实现一个支持接受参数包的list_node构造初始化。由于是万能引用,所以左值和右值都能接受,所以函数内部需要用完美转发传递参数。
这样就可以在调用push_back时有push_back的万能引用接受->insert的万能引用->list_node普通版本的构造初始化。
调用emplace_back时有emplace_back的万能引用接受->emplace的万能引用->list_node参数包版本的构造初始化。
template<class T>
struct list_node
{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& x = T()):_data(x), _next(nullptr), _prev(nullptr){}list_node(T&& x):_data(move(x)), _next(nullptr), _prev(nullptr){}template<class... Args>list_node(Args&&... args):_data(forward<Args>(args)...), _next(nullptr), _prev(nullptr){}
};template<class T>
class list
{typedef list_node<T> Node;
public:typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;//万能引用template<class X>void push_back(X&& x){insert(end(), forward<X>(x));}template<class X>iterator insert(iterator pos, X&& val){Node* cur = pos._node;Node* newnode = new Node(forward<X>(val));Node* prev = cur->_prev; //用临时变量避免后面用两个箭头prev->_next = newnode;newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;_size++;//单参数构造的隐式类型转换return newnode;}template<class... Args>void emplace_back(Args&&... args){emplace(end(), forward<Args>(args)...);}template<class... Args>iterator emplace(iterator pos, Args&&... args){Node* cur = pos._node;Node* newnode = new Node(forward<Args>(args)...);Node* prev = cur->_prev; //用临时变量避免后面用两个箭头prev->_next = newnode;newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;_size++;//单参数构造的隐式类型转换return newnode;}
};
二、新的类功能
默认的移动构造和移动赋值
- 首先我们要明确,移动构造对于内置类型和没有资源的浅拷贝自定义类型没有意义,因为移动构造的核心意义就是为了避免昂贵的深拷贝操作,转而通过 “转移资源所有权”
的方式,用轻量的浅拷贝对内置类型成员的简单赋值)完成对象的构造,所以对于内置类型或无资源的浅拷贝类型,由于拷贝本身已经足够廉价,且没有可转移的资源,移动构造不会带来任何性能提升或行为优化,因此没有实际意义。- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
- 如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动构造(只要是深拷贝类型或成员变量是深拷贝类型就一定会实现这三个函数,换言之只要是深拷贝类型或成员变量是深拷贝类型就需要自己实现移动构造)。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
- 如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。
class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}/*Person(const Person& p):_name(p._name),_age(p._age){}*//*Person& operator=(const Person& p){if(this != &p){_name = p._name;_age = p._age;}return *this;}*//*~Person()
{}*/
private:wusaqi::string _name;int _age;
};int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}
成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表⽤的,如果没有显⽰在初始化列表初始化,就会在初始化列表⽤这个缺省值初始化,这个我们在类和对象部分讲过了。
default和delete
- C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认生成的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显⽰指定移动构造⽣成。
- 如果能想要限制某些默认函数的⽣成,在C++98中,是将该函数设置成private,但是这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指⽰编译器不⽣成对应函数的默认版本,并且禁止该函数被任何形式调用,称=delete修饰的函数为删除函数。例如io流就会禁用掉拷贝函数,它会在拷贝函数声明加上=delete。
- 若我们要追求跨平台兼容性或严格标准符合性,下面的示例代码推荐显式定义移动构造函数,而非依赖 =default。因为在vs2022中用户定义了拷贝构造,若用 =default 声明移动构造,且类的成员(如代码中的 wusaqi::string)支持移动构造,MSVC 会生成有效的移动构造,这其实是因为MSVC做了易用性扩展。而C++标准是当用户定义拷贝构造后,=default 生成的移动构造通常会被标记为「删除(deleted)」,此时下面代码的std::move 会退化为调用拷贝构造,GCC/Clang 就会严格遵循标准,
Person s3 = std::move(s1);
的运行结果就是调用的拷贝构造。
class Person
{
public://构造函数Person(const char* name = "", int age = 0):_name(name), _age(age){}//拷贝构造函数Person(const Person& p):_name(p._name), _age(p._age){}//因为实现了拷贝构造函数,所以编译器不会自动生成移动构造函数//这里可以用default显⽰指定移动构造自动⽣成Person(Person&& p) = default;//Person(const Person& p) = delete;
private:wusaqi::string _name;int _age;
};int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}
final与override
这个我们在继承和多态章节已经进⾏了详细讲解,感兴趣的读者移步:点这里
以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~