C++进阶——C++11_右值引用和移动语义_可变参数模板_类的新功能

目录

1、右值引用和移动语义

1.1 左值和右值

1.2 左值引用和右值引用

1.3 引用延长生命周期

1.4 左值和右值的参数匹配

1.5 右值引用和移动语义的使用场景

1.5.1 左值引用主要使用场景

1.5.2 移动构造和移动赋值

1.5.3 右值引用和移动语义解决传值返回问题

1.5.4 右值引用和移动语义在传参中的提效

总结一下:

1.6 类型分类(了解)

1.7 引用折叠

1.7.1 语义折叠的概念

1.7.2 引用折叠的应用

1.8 完美转发

2、可变参数模板

2.1 基本语法及原理

2.2 包扩展

2.2.1 直接扩展

2.2.2 递归扩展

2.2.3 函数调用式包扩展

2.3 emplace系列接口

3、类的新功能

3.1 默认的移动构造和移动赋值

3.2 成员变量声明时给缺省值

3.3 default和delete

3.4 final和override


1、右值引用和移动语义

C++11之后中新增了的右值引用语法特性,C++11之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。 

1.1 左值和右值

左值是一个表示数据的表达式(如变量名解引用的指针等),一般是有持久状态存储在内存中可修改,我们可以获取它的地址左值可以出现在 " = " 的左边或右边。定义时const修饰后的左值,不能修改,但是可以取它的地址。

右值也是一个表示数据的表达式(如字面值常量临时对象等),一般没有名称没有内存地址不可修改右值不能取地址右值只能出现在 " = " 的右边

左值(lvalue)传统解释是 left valueC++11之后,更准确地解释为 locator value (定位值可以取地址)

右值(rvalue)传统解释是 right valueC++11之后,更准确地解释为 read value (可读值不可取地址)

#include<iostream>
using namespace std;
int main()
{// 左值:可以取地址// 以下的 p、b、c、*p、s、s[0] 就是常见的左值int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("111111");s[0] = 'x';cout << &c << endl;cout << (void*)&s[0] << endl; // cout打印char*是打印字符串,转成void*才能打印地址// 右值:不能取地址// 以下几个 10、x + y、fmin(x, y)、string("11111") 都是常见的右值double x = 1.1, y = 2.2;10;x + y;fmin(x, y);string("11111");// cout << &10 << endl;// cout << &(x+y) << endl;// cout << &(fmin(x, y)) << endl;// cout << &string("11111") << endl;return 0;
}

1.2 左值引用和右值引用

Type& r1 = x; 就是左值引用,给左值取别名

Type&& rr1 = y; 就是右值引用,给右值取别名

左值引用不能直接引用右值,但是const左值引用可以引用右值

右值引用不能直接引用左值,但是右值引用可以引用move(左值)

template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{// forward _Arg as movablereturn static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

move是库里面的一个函数模板,本质内部是进行强制类型转换(左值->右值)标记可窃取资源的对象,也可以move(右值),还是右值(增强可读性),当然还涉及一些引用折叠的知识,这个后面会细讲。

注意:变量表达式都是左值属性,那么,左值引用和右值引用本身是左值可以被修改,如:上面的r1是左值引用,rr1是右值引用,但本身都是左值,可以被修改,那么右值(一般不可修改)就可以通过右值引用进行修改,就可以达到窃取资源的目的

语法层面看,左值引用和右值引用都是取别名不开空间。从汇编底层的角度看下面代码中r1rr1的汇编层实现,底层都是用指针实现的,所以左值->右值。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要混到一起理解,互相佐证反而会陷入迷途。

#include<iostream>
using namespace std;int main()
{// 左值:可以取地址// 以下的p、b、c、*p、s、s[0]就是常见的左值int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("111111");s[0] = 'x';double x = 1.1, y = 2.2;// 左值引用给左值取别名int& r1 = b;int*& r2 = p;int& r3 = *p;string& r4 = s;char& r5 = s[0];// 右值引用给右值取别名int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);string&& rr4 = string("11111");// 左值引用不能直接引用右值,但是const左值引用可以引用右值const int& rx1 = 10;const double& rx2 = x + y;const double& rx3 = fmin(x, y);const string& rx4 = string("11111");// 右值引用不能直接引用左值,但是右值引用可以引用move(左值)int&& rrx1 = move(b);int*&& rrx2 = move(p);int&& rrx3 = move(*p);string&& rrx4 = move(s);string&& rrx5 = (string&&)s; // 强制类型转化// b、r1、rr1都是变量表达式,都是左值cout << &b << endl;cout << &r1 << endl;cout << &rr1 << endl;int& r6 = r1;// 这里要注意的是,rr1的属性是左值,所以不能再被右值引用绑定,除非move一下// int&& rrx6 = rr1;int&& rrx6 = move(rr1);return 0;
}

1.3 引用延长生命周期

注意:引用延长生命周期只能延长当前作用域生命周期

右值引用可用于延长临时对象的生命周期,右值引用本身是左值,可以被修改

const的左值引用也能延长临时对象的生命周期,但这些对象无法被修改

#include <iostream>
#include <string>int main()
{std::string s1 = "Test";const std::string& r2 = s1 + s1;       // OK:const左值引用可延长临时对象生命周期// r2 += "Test";                       // 错误:不能通过const引用修改std::string&& r3 = s1 + s1;            // OK:右值引用延长临时对象生存期r3 += "Test";                          // OK:可通过非const右值引用修改std::cout << r3 << '\n';return 0;
}

1.4 左值和右值的参数匹配

C++98中,我们实现一个const左值引用作为形参的函数,那么实参传递左值和右值都可以匹配

C++11以后,分别重载左值引用const左值引用右值引用作为形参的 f 函数,会调用最匹配的

#include <iostream>
using namespace std;void f(int& x) {cout << "左值引用重载 f(" << x << ")\n";
}void f(const int& x) {cout << "const左值引用重载 f(" << x << ")\n";
}void f(int&& x) {cout << "右值引用重载 f(" << x << ")\n";
}int main() {int i = 1;const int ci = 2;f(i);             // 调用 f(int&)f(ci);            // 调用 f(const int&)f(3);             // 调用 f(int&&),若无此重载则调用 f(const int&)f(std::move(i));  // 调用 f(int&&)// 右值引用变量在表达式中是左值int&& x = 1;f(x);             // 调用 f(int&)f(std::move(x));  // 调用 f(int&&)return 0;
}

1.5 右值引用和移动语义的使用场景

1.5.1 左值引用主要使用场景

左值引用主要使用场景是在函数中左值引用传参左值引用传返回值减少拷贝,同时还可以修改实参和修改返回对象的价值。

但是有些场景不能使用传左值引用返回,如 addStrings 和 generate 函数,C++98 中的解决方案只能是被迫使用输出型参数解决。那么 C++11 以后这里可以使用右值引用做返回值解决吗?显然是不能的,因为这里的本质是返回对象是一个局部对象函数结束这个对象就析构销毁了,右值引用返回,只能延长对象在当前函数栈帧的生命周期,但函数栈帧已经销毁了,对象会析构,无力回天了。

class Solution {
public:// 传值返回需要拷贝string addStrings(string num1, string num2) {string str;int end1 = num1.size() - 1;int end2 = num2.size() - 1;// 进位int next = 0;while (end1 >= 0 || end2 >= 0) {int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1) {str += '1';}reverse(str.begin(), str.end());return str;}// 这里的传值返回拷贝代价就太大了vector<vector<int>> generate(int numRows) {vector<vector<int>> vv(numRows);for (int i = 0; i < numRows; ++i){vv[i].resize(i + 1, 1);}for (int i = 2; i < numRows; ++i){for (int j = 1; j < i; ++j){vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}return vv;}
};
1.5.2 移动构造和移动赋值

移动构造是一种构造函数类似拷贝构造函数

移动构造函数要求第一个参数该类类型对象右值引用,后面只能加缺省参数。

移动赋值是一个赋值运算符重载类似拷贝赋值函数

移动赋值函数要求第一个参数该类类型对象右值引用

对于像 string/vector 这样的深拷贝的类或者包含深拷贝的成员变量的类移动构造和移动赋值才有意义,因为移动构造移动赋值第一个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源(右值对象一般是临时对象(返回的局部对象也认为是临时对象),直接swap临时对象的资源,不走深拷贝),从提高效率

注意:

1. move(左值)是让左值->右值本身没有窃取资源,是移动构造和移动赋值窃取(swap)资源

2. 对于内置类型,只需赋值就行,就算是移动,窃取的本质也是赋值,所以不需要移动语义

3. 个人疑惑,移动构造为什么不叫移动拷贝构造,不是用右值对象构造的吗?因为拷贝有不改变原对象的意思,避免混淆。

下面的 Lzc::string 样例实现了移动构造和移动赋值,我们需要结合场景理解。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
#include <string.h>
#include <algorithm>
using namespace std;namespace Lzc
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin() { return _str; }iterator end() { return _str + _size; }const_iterator begin() const { return _str; }const_iterator end() const { return _str + _size; }string(const char* str = ""): _size(strlen(str)), _capacity(_size){cout << "string(char* str) 构造" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}string(const string& s){cout << "string(const string& s) -- 拷贝构造" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}// 移动构造string(string&& s){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}string& operator=(const string& s){cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}}return *this;}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}~string(){cout << "~string() -- 析构" << endl;delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];if (_str){strcpy(tmp, _str);delete[] _str;}_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const { return _str; }size_t size() const { return _size; }private:char* _str = new char('\0');size_t _size = 0;size_t _capacity = 0;};
}int main()
{// 构造Lzc::string s1("xxxxx");// 拷贝构造Lzc::string s2 = s1;// 构造+移动构造,优化后直接构造Lzc::string s3 = Lzc::string("yyyyy");// 移动构造Lzc::string s4 = move(s1);cout << "******************************" << endl;return 0;
}
1.5.3 右值引用和移动语义解决传值返回问题
namespace Lzc {string addStrings(string num1, string num2) {string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;int next = 0;while (end1 >= 0 || end2 >= 0) {int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1)str += '1';reverse(str.begin(), str.end());cout << "******************************" << endl;return str;}
}int main() {Lzc::string ret;ret = Lzc::addStrings("11111", "2222");cout << ret.c_str() << endl;return 0;
}
g++ -std=c++11 test.cpp -fno-elide-constructors -o test && ./test

使用C++11标准,去掉编译器优化,编译为test,并执行。

1.5.4 右值引用和移动语义在传参中的提效

查看STL文档我们发现,C++11以后容器的push系列insert系列的接口都增加了右值引用版本。

实参是一个左值时,左值引用,容器内部继续调用拷贝构造进行拷贝,将拷贝的对象放到容器空间中。

实参是一个右值时,右值引用,容器内部则调用移动构造(由于右值引用本身是左值,会走拷贝构造,需move,转成右值,走移动构造),将窃取临时对象的资源的对象放到容器空间中。

	template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& data = T()):_data(data) // 拷贝构造,_next(nullptr),_prev(nullptr){}list_node(T&& data):_data(move(data))// 移动构造, _next(nullptr), _prev(nullptr){}};

其实这里还有一个emplace系列的接口,但是这个涉及可变参数模板,我们需要把可变参数模板讲解以后再讲解emplace系列的接口。

总结一下:

编译器优化不是C++的标准取决于编译器

右值引用+移动语义编译器优化差别不大有时略胜一筹只需支持C++11的右值引用和移动语义不依赖编译器

但是右值引用+移动语义 与 编译器优化 = 完美

多嘴一句:因为之前的C++委员会有点"摆烂",没有出右值引用和移动语义,所以编译器自己优化,所以现在理解有点难受。

1.6 类型分类(了解)

中文:值类别 - cppreference.com

英文:Value categories - cppreference.com

一般看,左值,右值。

  • 左值(lvalue) 是指具有持久性有名字的表达式可以取地址,通常代表一个对象的内存位置。

  • 纯右值(prvalue) 是临时对象,通常是计算过程中产生的中间结果,没有名字不能取地址

  • 将亡值(xvalue) 是即将被move的对象,通常是“可以被窃取资源”的右值。

1.7 引用折叠

1.7.1 语义折叠的概念

1. C++不能直接定义引用的引用,如 int& && r = i; 这样写会直接报错

2. 模板typedef中的类型操作可以构成引用的引用。 这时C++11给出了一个引用折叠的规则:右值引用右值引用折叠成右值引用所有其他组合均折叠成左值引用。 

#include <iostream>// 由于引用折叠限定,f1 实例化以后总是一个左值引用
template<class T>
void f1(T& x) {}// 由于引用折叠限定,f2 实例化后可以是左值引用,也可以是右值引用
// 也称: 万能引用
template<class T>
void f2(T&& x) {}int main() {typedef int& lref;typedef int&& rref;int n = 0;// 引用折叠示例lref& r1 = n;  // r1 的类型是 int&lref&& r2 = n; // r2 的类型是 int&rref& r3 = n;  // r3 的类型是 int&rref&& r4 = 1; // r4 的类型是 int&&// f1 函数模板实例化与调用情况// 没有折叠 -> 实例化为 void f1(int& x)f1<int>(n);// f1<int>(0);  // 报错,不能将右值绑定到左值引用// 折叠 -> 实例化为 void f1(int& x)f1<int&>(n);// f1<int&>(0); // 报错,不能将右值绑定到左值引用// 折叠 -> 实例化为 void f1(int& x)f1<int&&>(n);// f1<int&&>(0); // 报错,不能将右值绑定到左值引用// 折叠 -> 实例化为 void f1(const int& x)f1<const int&>(n);f1<const int&>(0);// 折叠 -> 实例化为 void f1(const int& x)f1<const int&&>(n);f1<const int&&>(0);// f2 函数模板实例化与调用情况// 没有折叠 -> 实例化为 void f2(int&& x)// f2<int>(n);   // 报错,不能将左值绑定到右值引用f2<int>(0);// 折叠 -> 实例化为 void f2(int& x)f2<int&>(n);// f2<int&>(0);  // 报错,不能将右值绑定到左值引用// 折叠 -> 实例化为 void f2(int&& x)// f2<int&&>(n); // 报错,不能将左值绑定到右值引用f2<int&&>(0);return 0;
}

个人疑惑:

const int&& x,x是右值引用,右值一般不可修改,因为x本身是左值,所以可以修改,如果还加const,那就是右值不可修改,这用说吗?而且只可以接受右值。

我const int& x,也是不可修改,还可以接受左值和右值。

但是一般使用函数模板,不显示实例化,

那么万能引用函数模板,的推导过程是:

传右值T就是右值的类型T&&就是右值引用

传左值T就是左值类型左值引用T&&就是左值引用。 

#include <iostream>
#include <utility> // movetemplate<class T>
void Function(T&& t) {int a = 0;T x = a;// x++;std::cout << &a << std::endl;std::cout << &x << std::endl << std::endl;
}int main() {// 10 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)Function(10);int a;// a 是左值,推导出 T 为 int&,引用折叠,模板实例化为 void Function(int& t)Function(a);// std::move(a) 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)Function(std::move(a));const int b = 8;// b 是 const 左值,推导出 T 为 const int&,引用折叠,模板实例化为 void Function(const int& t)// Function 内部会编译报错,因为 x 不能 ++Function(b);// std::move(b) 是 const 右值,推导出 T 为 const int,模板实例化为 void Function(const int&& t)// 所以 Function 内部会编译报错,x 不能 ++Function(std::move(b));return 0;
}
1.7.2 引用折叠的应用

左值引用和右值引用的函数,只有参数部分不同,函数体基本相同,高度相似->模板

例:

template<class T> list{ };中的push_back,此时T&&不是万能引用,因为list模板实例化了,T就已经确定了。要再加一层模板,才能构成万能引用。

            void push_back(const T& x){insert(end(), x);}void push_back(T&& x){insert(end(), move(x)); // x本身是左值}

那么list模板里面再来个函数模板,这个时候万能引用就体现出价值了,

传左值就实例化左值引用版本传右值就实例化右值引用版本

但是有一个问题不能直接move(x),因为可能是左值引用版本,若move(x),原对象的资源可能被窃取,改变了原对象。

  • 左值 → 会调用拷贝语义(保留原对象)。

  • 右值 → 会调用移动语义(允许“窃取”资源)。

		// 万能引用template<class X>void push_back(X&& x){insert(end(), x);}

这个时候就需要完美转发了,右值引用(本身是左值)返回右值引用,左值引用返回左值引用,然后再传。 

其实,这个逻辑没错,但是例子有点小瑕疵,如果是list<pair<int,int>>,没有万能引用,一开始就确定了T是pair<int,int>,可以push_back({1,2}),走类型转换,但是如果写成万能引用,就不能push_back({1,2}),因为在传参时,确定类型,不知道{1,2}是什么类型。

1.8 完美转发

// 左值版本(T 是具体类型,如 int&)
template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type& t) noexcept {return static_cast<T&&>(t);
}// 右值版本(T 是具体类型,如 int&&)
template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type&& t) noexcept {static_assert(!std::is_lvalue_reference<T>::value, "Cannot forward rvalue as lvalue");return static_cast<T&&>(t);
}

std::remove_reference_t<T>& 和 typename std::remove_reference<T>::type& 在功能上是完全等价的 

形式说明引入标准
typename std::remove_reference<T>::type&传统的 traits 用法,需要 typename 关键字C++98/03
std::remove_reference_t<T>&C++14 引入的简化写法,_t 后缀表示直接取类型C++14

remove_reference_t<T>:T变成非引用类型(去掉了传过来的T中的&)。

static_cast<T&&>(x):T&&会引用折叠,x强制转成T&&类型

当 T 是非引用类型时(即原始参数是右值),选择过程如下:

情况1:传入 右值(如 std::forward<int>(10)

  1. 模板参数 T 被推导为 int(非引用类型)

  2. 匹配过程:

    • 左值版本参数:remove_reference_t<int>& → int&
      ❌ 无法绑定到右值 10

    • 右值版本参数:remove_reference_t<int>&& → int&&
      ✅ 精确匹配右值

  3. 选择右值版本

  static_cast<T&&> → 生成右值引用类型表达式

     编译器魔法这个表达式会被特殊标记为"可以匹配右值引用参数"。

情况2:传入 左值(如 int x; std::forward<int&>(x)

  1. 模板参数 T 被推导为 int&(左值引用)

  2. 匹配过程:

    • 左值版本参数:remove_reference_t<int&>& → int&
      ✅ 精确匹配左值

    • 右值版本参数:remove_reference_t<int&>&& → int&&
      ❌ 无法绑定左值

  3. 选择左值版本

static_cast<T&&> → 生成左值引用类型表达式

所以:

		// 万能引用template<class X>void push_back(X&& x){insert(end(), forward<T>(x));}

注意:万能引用进行传参时,通常需要完美转发

    2、可变参数模板

    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) {}

    我们用省略号表示一个模板参数或函数参数的一个

    模板参数列表中,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:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能支持
    // 这里的功能,有了可变参数模板,我们进一步被解放,他是类型泛化基础
    // 上叠加数量变化,让我们泛型编程更灵活。
    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);
    // ...

    2.2 包扩展

    对于一个参数包,我们除了能计算它的参数个数,我们能做的唯一的事情就是扩展它

    // 可变模板参数
    // 参数类型可变
    // 参数个数可变// 打印参数包内容// template <class... Args>
    // void Print(Args... args)
    // {
    //     可变参数模板编译时解析
    //             
    //     下面是运行获取和解析,所以不支持这样用
    //     cout << sizeof...(args) << endl;
    //     for (size_t i = 0; i < sizeof...(args); i++)
    //     {
    //         cout << args[i] << " ";  // 不支持这样用
    //     }
    //     cout << endl;
    // }
    2.2.1 直接扩展
    template<class... Args>
    void Print(Args... args) {// 完全展开:生成与args数量相同的参数SomeFunc(args...); 
    }Print(1, "abc", 2.0); 
    // 展开为:SomeFunc(1, "abc", 2.0);
    2.2.2 递归扩展

    模板是写给编译器的。

    #include <iostream>
    #include <string>using namespace std;void ShowList() {// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数cout << endl;
    }// 传过来的args,是N个参数的参数包
    // 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
    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() {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);
    // }
    2.2.3 函数调用式包扩展

    和直接扩展相比,可以对每个参数预处理

    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必须返回接收到的对象,这样才能组成参数包给ArgumentsArguments(GetArg(args)...);
    }// void Print(int x, string y, double z)
    // {
    // }
    // 本质可以理解为编译器编译时,包的扩展模式
    // 将上面的函数模板扩展实例化为下面的函数
    // 是不是很抽象,C++11以后,只能说委员会的大佬设计语法思维跳跃得太厉害
    // Arguments(GetArg(x), GetArg(y), GetArg(z));int main() {Print(1, string("xxxxx"), 2.2);return 0;
    }

    2.3 emplace系列接口

    template <class... Args> void emplace_back (Args&&... args);
    template <class... Args> iterator emplace (const_iterator position
    , Args&&... args);

    C++11 以后 STL 容器新增了 emplace 系列的接口,emplace 系列的接口均为可变参数模板,功能上兼容 pushinsert 系列。假设容器为 container<T>,emplace 还支持直接插入构造 T 对象的参数,可以直接在容器空间上构造 T 对象,高效一些。

    下面我们模拟实现了 list emplace emplace_back 接口,这里把参数包不断往下传递,最终在节点的构造中直接去匹配容器存储的数据类型 T 构造可以直接在容器空间上构造 T 对象

    传递参数包过程中,如果是Args&&... args的万能引用参数包,要用完美转发参数包,方式如下
    std::forward<Args>(args)...std::forward 分别应用到 args 中的每一个参数上。否则编译时包扩展后右值引用变量表达式就变成了左值。 

    emplace直接在容器内构造对象,避免临时对象的创建和拷贝/移动,因此在多数情况下比push/insert高效

    如:下面的push_back和insert没有实现万能引用,对于list<pair<int,int>>,push_back({1,2})可以类型转换,万能引用就不能类型转换(不知道{1,2}是什么类型),更灵活。

    #pragma once
    #include<assert.h>namespace Lzc
    {template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node() = default;template <class... Args>list_node(Args&&... args): _next(nullptr), _prev(nullptr), _data(std::forward<Args>(args)...){}};template<class T, class Ref, class Ptr>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;Node* _node;list_iterator(Node* node):_node(node){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}Self& operator++(){_node = _node->_next;return *this;}Self& operator--(){_node = _node->_prev;return *this;}Self operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self& operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const Self& s) const{return _node != s._node;}bool operator==(const Self& s) const{return _node == s._node;}};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;iterator begin(){return _head->_next;}iterator end(){return _head;}const_iterator begin() const{return _head->_next;}const_iterator end() const{return _head;}void empty_init(){_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}list(){empty_init();}list(initializer_list<T> il){empty_init();for (auto& e : il){push_back(e);}}// lt2(lt1)list(const list<T>& lt){empty_init();for (auto& e : lt){push_back(e);}}// lt1 = lt3list<T>& operator=(list<T> lt){swap(lt);return *this;}~list(){clear();delete _head;_head = nullptr;}void clear(){auto it = begin();while (it != end()){it = erase(it);}}void swap(list<T>& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}void push_back(const T& x){insert(end(), x);}void push_back(T&& x){insert(end(), forward<T>(x));}// 万能引用/*template<class X>void push_back(X&& x){insert(end(), forward<X>(x));}*/template <class... Args>void emplace_back(Args&&... args){insert(end(), std::forward<Args>(args)...);}void push_front(const T& x){insert(begin(), x);}iterator insert(iterator pos, const T& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);// prev newnode curnewnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;++_size;return newnode;}iterator insert(iterator pos, T&& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(forward<T>(x));// prev newnode curnewnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;++_size;return newnode;}// 万能引用//template<class X>//iterator insert(iterator pos, X&& x)//{//	Node* cur = pos._node;//	Node* prev = cur->_prev;//	Node* newnode = new Node(forward<X>(x));//	// prev newnode cur//	newnode->_next = cur;//	cur->_prev = newnode;//	newnode->_prev = prev;//	prev->_next = newnode;//	++_size;//	return newnode;//}template <class... Args>iterator insert(iterator pos, Args&&... args){Node* cur = pos._node;Node* newnode = new Node(std::forward<Args>(args)...);Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}iterator erase(iterator pos){assert(pos != end());Node* prev = pos._node->_prev;Node* next = pos._node->_next;prev->_next = next;next->_prev = prev;delete pos._node;--_size;return next;}size_t size() const{return _size;}bool empty() const{return _size == 0;}private:Node* _head;size_t _size;};
    }

    3、类的新功能

    3.1 默认的移动构造和移动赋值

    原来 C++ 类中,有 6 个默认成员函数:构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 / 取地址重载 /const 取地址重载,最重要的是前 4 个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增两个默认成员函数移动构造函数移动赋值运算符重载

    如果你自己没有实现移动构造函数,没有实现析构函数拷贝构造拷贝赋值重载中的任意一
    (因为这几个是绑定到一起的,都不写,说明默认生成的就够用了)。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执
    行逐成员按字节拷贝(浅拷贝),自定义类型成员,如果实现了移动构造调用移动构造没有实现调用拷贝构造

    如果你自己没有实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意
    一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会
    执行逐成员按字节拷贝(浅拷贝),自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值移动构造完全类似)

    如果你自己实现移动构造或者移动赋值编译器不会自动提供拷贝构造和拷贝赋值

    3.2 成员变量声明时给缺省值

    C++初阶——类和对象(下)-CSDN博客

    3.3 default和delete

    C++11 可以让你更好地控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成,可以使用 default 关键字显式指定生成,比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显式指定移动构造生成

    C++11如果想要限制某些默认函数的生成,只需在该函数声明加上 = delete ,该语法指示编译器不生成对应函数的默认版本,称 = delete 修饰的函数删除函数

    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(Person&& p) = default;// Person(const Person& p) = delete;private:bit::string _name;int _age;
    };int main() {Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
    }

    3.4 final和override

    C++进阶——多态-CSDN博客

    本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/77487.shtml

    如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

    相关文章

    HTTP协议原理深度解析:从基础到实践

    引言 在互联网技术体系中,HTTP(HyperText Transfer Protocol)协议如同数字世界的"通用语言",支撑着全球超50亿网民的日常网络交互。作为爬虫开发、Web应用构建的核心技术基础,理解HTTP原理是每个开发者必须掌握的技能。本文将从协议本质、技术演进、安全机制三…

    Web品质 - 重要的HTML元素

    Web品质 - 重要的HTML元素 在构建一个优秀的Web页面时,HTML元素的选择和运用至关重要。这些元素不仅影响页面的结构,还直接关系到页面的可用性、可访问性和SEO表现。本文将深入探讨一些关键的HTML元素,并解释它们在提升Web品质方面的重要性。 1. <html> 根元素 HTM…

    【AI提示词】竞品分析专家

    提示说明 对产品进行竞品分析&#xff0c;明确产品定位和优化营销策略。 提示词 # 角色:竞品分析专家## 背景: 需要对旗下产品A进行竞品分析,明确产品定位和优化营销策略。## 描述: - 作者:张三 - 版本:1.0 - 语言:中文## 注意事项: 保持客观公正态度,用数据说话,给出具体的…

    4-6记录(B树)

    找左边右下或者右边左下 转化成了前驱后继的删除 又分好几种情况&#xff1a; 1. 只剩25&#xff0c;小于2&#xff0c;所以把父亲拉到25旁边&#xff0c;兄弟的70顶替父亲 对于25&#xff0c;25的后继就是70&#xff0c;25后继的后继是71&#xff08;中序遍历) 2. 借左子树…

    什么是RACI矩阵,应用在什么场景?

    一、什么是RACI RACI矩阵是一种用于明确项目或任务中角色与责任的管理工具&#xff0c;通过定义不同人员在任务中的参与程度来避免职责不清的问题。以下是其核心要点&#xff1a; ‌RACI的含义‌ ● ‌R&#xff08;Responsible&#xff09;执行者‌&#xff1a;直接完成任务…

    深入理解全排列算法:DFS与回溯的完美结合

    全排列问题是算法中的经典问题&#xff0c;其目标是将一组数字的所有可能排列组合列举出来。本文将详细解析如何通过深度优先搜索&#xff08;DFS&#xff09;和回溯法高效生成全排列&#xff0c;并通过模拟递归过程帮助读者彻底掌握其核心思想。 问题描述 给定一个正整数 n&a…

    在 Dev-C++中编译运行GUI 程序介绍(二)示例:祝福程序

    在 Dev-C中编译运行GUI 程序介绍&#xff08;二&#xff09;示例&#xff1a;祝福程序 前期见&#xff1a; 在 Dev-C中编译运行GUI 程序介绍&#xff08;一&#xff09;基础 https://blog.csdn.net/cnds123/article/details/147019078 示例1、祝福程序 本文中的这个祝福程序是…

    Stable Diffusion 四重调参优化——项目学习记录

    学习记录还原&#xff1a;在本次实验中&#xff0c;我基于 Stable Diffusion v1.5模型&#xff0c;通过一系列优化方法提升生成图像的质量&#xff0c;最终实现了图像质量的显著提升。实验从基础的 Img2Img 技术入手&#xff0c;逐步推进到参数微调、DreamShaper 模型和 Contro…

    Solidity智能合约漏洞类型与解题思路指南

    一、常见漏洞类型与通俗解释 1. 重入攻击(Reentrancy) 🌀 通俗解释:就像你去银行取钱,柜台人员先给你钱,然后再记账。你拿到钱后立即又要求取钱,由于账还没记,柜台又给你一次钱,这样循环下去你就能拿走银行所有的钱。 漏洞原理:合约在更新状态前调用外部合约,允许…

    Docker部署.NetCore8项目

    在VS.net新建.netCore8项目&#xff0c;生成项目的发布文件&#xff0c;之后添加Dockerfile&#xff0c;内容如下&#xff1a; FROM mcr.microsoft.com/dotnet/aspnet:8.0 # 设置工作目录 WORKDIR /app # 挂载临时卷&#xff08;类似于 VOLUME /tmp&#xff09; VOLUME /tmp …

    【C++】右值引用、移动语义与完美转发

    左值、右值是C常见的概念&#xff0c;那么什么是右值引用&#xff0c;移动语义&#xff0c;完美转发呢&#xff1f;本UP带大家了解一下C校招常问的C11新特性。 左值与右值 左值&#xff1a;明确存储未知、可以取地址的表达式 右值&#xff1a;临时的、即将被销毁的&#xff…

    艾尔登法环地图不能使用鼠标移动或点击传送点原因和设置方法

    今天玩艾尔登法环突发发现地图不能用鼠标点击传送点了。 找了半天发现设置地图选单的游标移动方式只有键盘了&#xff0c;改成键盘与鼠标就好啦。

    【算法】——一键解决动态规划

    前言 动态规划是一种高效解决​​重叠子问题​​和​​最优子结构​​问题的算法思想。它通过​​分治记忆化​​&#xff0c;将复杂问题分解为子问题&#xff0c;并存储中间结果&#xff0c;避免重复计算&#xff0c;从而大幅提升效率。 ​​为什么重要&#xff1f;​ ​​优化…

    uniApp开发微信小程序-连接蓝牙连接打印机上岸!

    历经波折三次成功上岸&#xff01; 三次经历简单絮叨一下&#xff1a;使用uniAppvue开发的微信小程序&#xff0c;使用蓝牙连接打印机&#xff0c;蓝牙所有的接口都是插件中封装的&#xff0c;用的插件市场中的这个&#xff1a; dothan-lpapi-ble &#xff1b;所以&#xff0c…

    软件系统安全设计方案,信息化安全建设方案(Word原件)

    1.1 总体设计 1.1.1 设计原则 1.2 物理层安全 1.2.1 机房建设安全 1.2.2 电气安全特性 1.2.3 设备安全 1.2.4 介质安全措施 1.3 网络层安全 1.3.1 网络结构安全 1.3.2 划分子网络 1.3.3 异常流量管理 1.3.4 网络安全审计 1.3.5 网络访问控制 1.3.6 完…

    wsl2+ubuntu22.04安装blenderproc教程

    本章教程,介绍如何在windows操作系统上通过wsl2+Ubuntu22.04上安装blenderproc。 一、pipi安装方式 推荐使用minconda3安装Python环境。 pip install Blenderproc二、源码安装 1、下载源码 git clone https://github.com/DLR-RM/BlenderProc2、安装依赖 cd BlenderProc &am…

    Blender 转 STL 文件全攻略:从基础到进阶

    在 3D 建模与打印领域&#xff0c;Blender 凭借其强大的功能和开源特性&#xff0c;深受创作者喜爱。而 STL 文件格式&#xff0c;作为 3D 打印行业的通用标准&#xff0c;能被绝大多数 3D 打印软件和设备所识别。因此&#xff0c;将 Blender 模型转换为 STL 文件&#xff0c;是…

    Ansys Electronics 变压器 ACT

    你好&#xff0c; 在本博客中&#xff0c;我将讨论如何使用 Ansys 电子变压器 ACT 自动快速地设计电力电子电感器或变压器。我将逐步介绍设计和创建电力电子变压器示例的步骤&#xff0c;该变压器为同心组件&#xff0c;双绕组&#xff0c;采用正弦电压激励&#xff0c;并应用…

    nacos配置达梦数据库驱动源代码步骤

    1.在父工程pom.xml添加依赖&#xff1a; <dependency><groupId>com.dameng</groupId><artifactId>DmJdbcDriver18</artifactId><version>8.1.1.193</version> </dependency> 2.在nacos-config模块pom.xml添加依赖&#xff1…

    4.9-4.10学习总结 Stream流练习+方法引用+异常

    Stream流练习&#xff1a; 1.打印数组内的偶数。 import java.util.*; import java.util.function.BiConsumer; public class test {public static void main(String[] args) {ArrayList<Integer> listnew ArrayList<>();Collections.addAll(list,1,2,3,4,5,6,7,…