[C.C++] 【C++:C++11】核心进阶:C++11引用折叠、完美转发与可变参数模板实战详解

399 0
Honkers 2026-4-17 12:59:11 来自手机 | 显示全部楼层 |阅读模式

🔥小叶-duck:个人主页

❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《算法题讲解指南》--优选算法
《算法题讲解指南》--递归、搜索与回溯算法
《算法题讲解指南》--动态规划算法

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一. 值类别细分:左值、泛左值、纯右值与将亡值

二. 引用折叠:万能引用底层逻辑

  1、引用折叠的核心规则

  2、万能引用的实际应用

三. 完美转发:保持值类别属性的关键

  1、结合引用折叠和完美转发优化模拟实现List代码

四. 可变参数模板:支持任意参数的泛型编程

  1、 基本语法

  2、示例演示

  3、参数包展开:编译时递归推导

五. 落地实践:emplace 系列接口的实现

  1、emplace_back 与 push_back 的区别

  2、模拟实现 list 的 emplace_back

结束语


前言

      C++11 引入的右值引用和移动语义解决了拷贝效率问题,但要真正灵活运用,还需掌握其延伸特性 —— 引用折叠完美转发可变参数模板。这三个特性是现代 C++ 泛型编程的基石,支撑了 万能引用、emplace 系列接口 等高频用法,也是理解 STL 容器底层优化的关键。

一. 值类别细分:左值、泛左值、纯右值与将亡值

      C++11 以后对传统的 “左值 / 右值” 进行了更精细的划分:

  • 泛左值(glvalue):包含左值和将亡值,核心特征是 “有标识(可寻址)”
  • 纯右值(prvalue):传统右值的核心部分,无标识、不可寻址。指那些字面值常量或者求值结果相当于字面值或是一个不具名的临时对象(匿名对象)(如字面量10、表达式x+y、临时对象string("hello"));
  • 将亡值(xvalue):返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达式(如 move(x)、static_cast(x) );
  • 核心关系:右值 = 纯右值 + 将亡值;泛左值 = 左值 + 将亡值。

参考文档:Value categories - cppreference.com

二. 引用折叠:万能引用底层逻辑

      C++ 不允许直接定义 “引用的引用” 如( int&& x ),但通过模版 typedef 间接构成时,会触发引用折叠规则,这也是"万能引用"的核心原理。

  1. typedef int& lref;
  2. typedef int&& rref;
  3. int n = 0;
  4. lref& r1 = n; // r1 的类型是 int&
  5. lref&& r2 = n; // r2 的类型是 int&
  6. rref& r3 = n; // r3 的类型是 int&
  7. rref&& r4 = 1; // r4 的类型是 int&&
复制代码

  1、引用折叠的核心规则

  • 右值引用的右值引用折叠为右值引用,其他所有组合均折叠为左值引用

      以 int 为例

原始类型组合折叠后类型
int& &int&
int& &&int&
int&& &int&
int&& &&int&&

实际示例

  1. // 由于引用折叠限定,f1形参类型为左值引用,导致实例化以后总会是⼀个左值引用
  2. template<class T>
  3. void f1(T& x)
  4. {
  5. }
  6. // 由于引用折叠限定,f2形参类型为右值引用,实例化后可以是左值引用,也可以是右值引用
  7. template<class T>
  8. void f2(T&& x)
  9. {
  10. }
  11. int main()
  12. {
  13. // 没有折叠->实例化为void f1(int& x)
  14. f1<int>(n);
  15. //f1<int>(0); // 报错
  16. // 折叠->实例化为void f1(int& x)
  17. f1<int&>(n);
  18. // f1<int&>(0); // 报错
  19. // 折叠->实例化为void f1(int& x)
  20. f1<int&&>(n);
  21. // f1<int&&>(0); // 报错
  22. // 折叠->实例化为void f1(const int& x)
  23. f1<const int&>(n);
  24. f1<const int&>(0); //const左值引用可以引用右值
  25. // 折叠->实例化为void f1(const int& x)
  26. f1<const int&&>(n);
  27. f1<const int&&>(0);
  28. // 没有折叠->实例化为void f2(int&& x)
  29. // f2<int>(n); // 报错
  30. f2<int>(move(n));
  31. f2<int>(0);
  32. // 折叠->实例化为void f2(int& x)
  33. f2<int&>(n);
  34. // f2<int&>(0); // 报错
  35. // 折叠->实例化为void f2(int&& x)
  36. // f2<int&&>(n); // 报错
  37. f2<int&&>(0);
  38. return 0;
  39. }
复制代码
  • f1我们就不用多说了,最终实例化都只会是一个左值引用
  • f2 这样的函数模版中,T&& x 参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有写地方也把这种函数模版的参数叫做万能引用
  1. // 万能引用-以int类型的为例
  2. // 传左值时,T被自动推导为int&,最后就是左值引用
  3. // 传右值时,T被自动推导为int,最后就是右值引用
  4. template<class T>
  5. void Function(T&& t)
  6. {
  7. int a = 0;
  8. T x = a;
  9. //x++;
  10. cout << &a << endl;
  11. cout << &x << endl << endl;
  12. }
  13. int main()
  14. {
  15. // 10是右值,推导出T为int,模板实例化为void Function(int&& t)
  16. Function(10); // 右值
  17. int a;
  18. // a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
  19. //这里一定要注意并不是a是什么类型T就是什么类型,由于引用折叠的特性,T的类型推导一定会符合实参的情况,
  20. //如果T为int则T&&就变成了int&&,则无法进行传参,而只有T为int&,T&&才是int&(引用折叠)实现传参
  21. //并且通过打印的a和x的地址是相同的也能证明
  22. Function(a); // 左值
  23. // std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
  24. Function(std::move(a)); // 右值
  25. const int b = 8;
  26. // a是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
  27. // 所以Function内部会编译报错,x不能++
  28. Function(b); // const 左值
  29. // std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
  30. // 所以Function内部会编译报错,x不能++
  31. Function(std::move(b)); // const 右值
  32. return 0;
  33. }
复制代码

  • Function(T&& t) 函数模版程序中,假设实参是 int 右值,模版参数T的推导就是 int ;实参是 int 左值,模版参数T的推导就是 int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参Function,实参是右值,实例化出右值引用版本形参的Function。

  2、万能引用的实际应用

      模板参数 T&& 并非单纯的右值引用,结合引用折叠后,可接收左值和右值,称为 “万能引用”:  

  • 当实参为左值(如int a),模板参数 T 推导为 int&,T&& 折叠为 int&(左值引用);
  • 当实参为右值(如10),模板参数 T 推导为 int,T&& 为 int&&(右值引用)。

     我们拿前面学习list容器时模拟实现过的 List.h 来改改看,虽然里面还涉及到完美转发,下面就会进行讲解,并且当讲完完美转发会把模拟实现的 List 整体进行进一步的优化。(当然库里的push_back 并没有做以下万能引用的实现,只是通过实际的应用场景给大家展示“万能引用”的巧妙之处,因为库里存在emplace_back()):

三. 完美转发:保持值类别属性的关键

      万能引用虽然可以接受任意值类别但右值变量本身是左值,直接传递会丢失原本的类别属性。所以我们需要完美转发 std::forward,确保参数在传递过程中保持原本属性

实际分析:

  • 上面实现过的 Function(T&& t) 函数模版中,传左值实例化以后是左值引用的 Function 函数,传右值实例化以后是右值引用的 Function 函数。
  • 但结合之前学过的,变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说 Function 函数中 t 自身的属性是左值,那么我们把 t 传递给下一层函数 Fun 时,就会导致匹配的都是左值引用版本的 Fun 函数,这就和我们的预期不符,因为我们传的右值,希望所有的函数调用过程都是走的右值引用,所以这里我们想要保持 t 对象的属性,就需要使用完美转发来实现。
  • 完美转发源码:通过 static_cast 结合引用折叠,精准还原参数的原始值类别。
  1. _EXPORT_STD template <class _Ty>
  2. _NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept {
  3. return static_cast<_Ty&&>(_Arg);
  4. }
  5. _EXPORT_STD template <class _Ty>
  6. _NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept {
  7. static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
  8. return static_cast<_Ty&&>(_Arg);
  9. }
复制代码

【完美转发】forward 本质是一个函数模板,主要还是通过引用折叠的方式实现,下面示例中传递给Function的实参是右值,模板参数T(type)被推导为整型int,没有折叠,forward内部 t 被强转为右值引用返回;传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部 t 被强转为左值引用返回。

  1. //==============================完美转发==============================
  2. void Fun(int& x) { cout << "左值引用" << endl; }
  3. void Fun(const int& x) { cout << "const 左值引用" << endl; }
  4. void Fun(int&& x) { cout << "右指引用" << endl; }
  5. void Fun(const int&& x) { cout << "const 右值引用" << endl; }
  6. //万能引用
  7. template<class T>
  8. void Function(T&& t)
  9. {
  10. Fun(forward<T>(t));
  11. }
  12. int main()
  13. {
  14. // 10是右值,推导出T为int,模板实例化为void Function(int&& t)
  15. Function(10); // 右值
  16. int a;
  17. // a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
  18. Function(a); // 左值
  19. // std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
  20. Function(std::move(a)); // 右值
  21. const int b = 8;
  22. // a是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int&t)
  23. Function(b); // const 左值
  24. // std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
  25. Function(std::move(b)); // const 右值
  26. return 0;
  27. }
复制代码

  1、结合引用折叠和完美转发优化模拟实现List代码

      在前面学习list容器时,我们对 list容器的模拟实现 进行了讲解,但当时由于还没有讲解C++11的知识,所以接口的模拟实现并没有涉及引用折叠和完美转发的概念,现在我们已经讲解了就可以进一步去优化当时模拟实现的list容器了。

  1. //List.h
  2. namespace xiaoye
  3. {
  4. class string
  5. {
  6. public:
  7. typedef char* iterator;
  8. typedef const char* const_iterator;
  9. iterator begin()
  10. {
  11. return _str;
  12. }
  13. iterator end()
  14. {
  15. return _str + _size;
  16. }
  17. const_iterator begin() const
  18. {
  19. return _str;
  20. }
  21. const_iterator end() const
  22. {
  23. return _str + _size;
  24. }
  25. string(const char* str = "")
  26. :_size(strlen(str))
  27. , _capacity(_size)
  28. {
  29. cout << "string(char* str)-构造" << endl;
  30. _str = new char[_capacity + 1];
  31. strcpy(_str, str);
  32. }
  33. ~string()
  34. {
  35. cout << "~string() -- 析构" << endl;
  36. delete[] _str;
  37. _str = nullptr;
  38. _size = _capacity = 0;
  39. }
  40. void swap(string& s)
  41. {
  42. std::swap(_str, s._str);
  43. std::swap(_size, s._size);
  44. std::swap(_capacity, s._capacity);
  45. }
  46. // 拷贝构造
  47. string(const string& s)
  48. {
  49. cout << "string(const string& s) -- 拷贝构造" << endl;
  50. reserve(s._capacity); //拷贝构造需要额外再开辟空间进行拷贝数据
  51. for (auto e : s)
  52. {
  53. push_back(e);
  54. }
  55. }
  56. // 移动构造
  57. string(string&& rs)
  58. {
  59. cout << "string(string&& s) -- 移动构造" << endl;
  60. swap(rs); //移动构造只需要"掠夺"右值引用形参的资源即可实现构造
  61. }
  62. //拷贝赋值
  63. string& operator=(const string& s)
  64. {
  65. cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
  66. if (this != &s)
  67. {
  68. _str[0] = '\n';
  69. _size = 0;
  70. reserve(s._capacity); //拷贝赋值需要额外再开辟空间进行拷贝赋值数据
  71. for (auto e : s)
  72. {
  73. push_back(e);
  74. }
  75. }
  76. return *this;
  77. }
  78. //移动赋值
  79. string& operator=(string&& rs)
  80. {
  81. cout << "string& operator=(string&& s) -- 移动赋值" << endl;
  82. swap(rs); //移动赋值只需要"掠夺"右值引用形参的资源即可实现赋值
  83. //并且不需要手动先删除旧数据,交换完后出了函数形参rs就会带着旧数据一起被销毁
  84. return *this;
  85. }
  86. void reserve(size_t new_capacity)
  87. {
  88. if (new_capacity > _capacity)
  89. {
  90. char* tmp = new char[new_capacity + 1];
  91. if (_str)//_str不为空才能进行delete,否则会报错
  92. {
  93. strcpy(tmp, _str);
  94. delete[]_str;
  95. }
  96. _str = tmp;
  97. _capacity = new_capacity;
  98. }
  99. }
  100. char& operator[](size_t pos)
  101. {
  102. assert(pos < _size);
  103. return _str[pos];
  104. }
  105. void push_back(char ch)
  106. {
  107. if (_size >= _capacity)
  108. {
  109. size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
  110. reserve(newcapacity);
  111. }
  112. _str[_size] = ch;
  113. ++_size;
  114. _str[_size] = '\0';
  115. }
  116. string& operator+=(char ch)
  117. {
  118. push_back(ch);
  119. return *this;
  120. }
  121. const char* c_str() const
  122. {
  123. return _str;
  124. }
  125. size_t size() const
  126. {
  127. return _size;
  128. }
  129. size_t capacity() const
  130. {
  131. return _capacity;
  132. }
  133. private:
  134. char* _str = nullptr;
  135. size_t _size = 0;
  136. size_t _capacity = 0;
  137. };
  138. //链表结点
  139. template<class T>
  140. struct ListNode
  141. {
  142. ListNode<T>* _next;
  143. ListNode<T>* _prev;
  144. T _data;
  145. ////左值引用版本的链表结点构造
  146. //ListNode(const T& data = T())
  147. // :_next(nullptr)
  148. // , _prev(nullptr)
  149. // , _data(data)
  150. //{
  151. //}
  152. ////右值引用版本的链表结点构造
  153. //ListNode(T&& data)
  154. // :_next(nullptr)
  155. // , _prev(nullptr)
  156. // , _data(forward<T>(data))
  157. //{
  158. //}
  159. //无参构造
  160. ListNode()
  161. :_next(nullptr)
  162. , _prev(nullptr)
  163. , _data(T())
  164. {
  165. }
  166. //万能引用
  167. //需要注意的是当实现成万能引用时就必须传入一个参数,否则无法将模板参数X进行实例化
  168. //而如果这样空链表的初始化就需要额外写一个无参的构造函数了
  169. template<class X>
  170. ListNode(X&& data)
  171. :_next(nullptr)
  172. , _prev(nullptr)
  173. , _data(forward<X>(data)) //拷贝构造或者移动构造
  174. //由于data自身属性是左值,为了成功调用合适的引用版本的构造我们就需要进行完美转发保持属性一致
  175. {
  176. }
  177. };
  178. //链表迭代器
  179. template<class T, class Ref, class Ptr>
  180. struct ListIterator
  181. {
  182. typedef ListNode<T> Node;
  183. typedef ListIterator<T, Ref, Ptr> Self;
  184. Node* _node;
  185. ListIterator(Node* node)
  186. :_node(node)
  187. {
  188. }
  189. Self& operator++()
  190. {
  191. _node = _node->_next;
  192. return *this;
  193. }
  194. Ref operator*()
  195. {
  196. return _node->_data;
  197. }
  198. Ptr operator->()
  199. {
  200. return &_node->_data;
  201. }
  202. bool operator!=(const Self& it)
  203. {
  204. return _node != it._node;
  205. }
  206. };
  207. //链表
  208. template<class T>
  209. class list
  210. {
  211. typedef ListNode<T> Node;
  212. public:
  213. typedef ListIterator<T, T&, T*> iterator;
  214. typedef ListIterator<T, const T&, const T*> const_iterator;
  215. iterator begin()
  216. {
  217. return iterator(_head->_next);
  218. }
  219. iterator end()
  220. {
  221. return iterator(_head);
  222. }
  223. list()
  224. {
  225. _head = new Node();
  226. _head->_next = _head;
  227. _head->_prev = _head;
  228. }
  229. ////左值引用版本的尾插
  230. //void push_back(const T& x)
  231. //{
  232. // insert(end(), x);
  233. //}
  234. ////右值引用版本的尾插
  235. //void push_back(T&& x)
  236. //{
  237. // insert(end(), forward<T>(x));
  238. //}
  239. ////这个并不是万能引用
  240. ////因为T是list的模板参数,当xiaoye::list<xiaoye::string>实例化后,参数T就已经确定为xiaoye::string了
  241. ////而并没有通过传入的实参类型为左值还是右值而进行二次推导的过程,
  242. ////所以我们不能只写这个右值引用版本,否则左值数据的插入就会找不到左值引用版本的尾插而报错
  243. //万能引用
  244. //在类模板list中在实现一个模板参数不同的函数模板,
  245. //这样也就能保证了只有当我们调用插入函数时才能根据插入数据的类型进行实例化,进而对模板参数X进行类型推导
  246. //这样也就将引用折叠和完美转发应用到list中了,不管传的是左值还是右值都只需要这一个函数模板即可
  247. template<class X>
  248. void push_back(X&& x)
  249. {
  250. insert(end(), forward<X>(x));
  251. }
  252. ////左值引用版本的插入
  253. //iterator insert(iterator pos, const T& x)
  254. //{
  255. // Node* cur = pos._node;
  256. // Node* newnode = new Node(x);
  257. // Node* prev = cur->_prev;
  258. // // prev newnode cur
  259. // prev->_next = newnode;
  260. // newnode->_prev = prev;
  261. // newnode->_next = cur;
  262. // cur->_prev = newnode;
  263. // return iterator(newnode);
  264. //}
  265. ////右值引用版本的插入
  266. //iterator insert(iterator pos, T && x)
  267. //{
  268. // Node* cur = pos._node;
  269. // Node* newnode = new Node(forward<T>(x));
  270. // Node* prev = cur->_prev;
  271. // // prev newnode cur
  272. // prev->_next = newnode;
  273. // newnode->_prev = prev;
  274. // newnode->_next = cur;
  275. // cur->_prev = newnode;
  276. // return iterator(newnode);
  277. //}
  278. //万能引用
  279. template<class X>
  280. iterator insert(iterator pos, X&& x)
  281. {
  282. Node* cur = pos._node;
  283. Node* newnode = new Node(forward<X>(x));
  284. //由于x自身属性是左值,创建节点时new一个Node,传入的数据也需要通过完美转发保持右值属性
  285. Node* prev = cur->_prev;
  286. // prev newnode cur
  287. prev->_next = newnode;
  288. newnode->_prev = prev;
  289. newnode->_next = cur;
  290. cur->_prev = newnode;
  291. return iterator(newnode);
  292. }
  293. private:
  294. Node* _head;
  295. };
  296. }
复制代码
  1. //Test.cpp
  2. #include"List.h"
  3. int main()
  4. {
  5. xiaoye::list<xiaoye::string> lt;
  6. cout << "*************************" << endl;
  7. xiaoye::string s1("111111111111111111111");
  8. lt.push_back(s1);
  9. cout << "*************************" << endl;
  10. lt.push_back(xiaoye::string("22222222222222222222222222222"));
  11. cout << "*************************" << endl;
  12. lt.push_back("3333333333333333333333333333");
  13. cout << "*************************" << endl;
  14. lt.push_back(move(s1));
  15. cout << "*************************" << endl;
  16. return 0;
  17. }
复制代码

注意的是:编译器的优化程度各不相同,可能和这里的结果有出入,如果对这里的结果怎么来的不清楚或者一知半解可以自己尝试通过调试看看是怎么执行代码的。

四. 可变参数模板:支持任意参数的泛型编程

      C++11 引入可变参数模板,允许模板参数和函数参数的个数可变,解决了 “支持任意参数个数 / 类型” 的泛型需求,是 emplace 系列接口printf 等函数的底层支撑

  • C+11 支持可变参数模版,也就是说支持可变数量参数的函数模版和类模版,可变数目的参数被称为参数包,存在两种参数包:模版参数包,表示零个或多个模版参数函数参数包:表示零个或多个函数参数
  1. template <class ...Args> void Func(Args... args) {}
  2. template <class ...Args> void Func(Args&... args) {}
  3. template <class ...Args> void Func(Args&&... args) {}
复制代码
  • 我们用省略号来指出一个模版参数或函数参数的表示一个包,在模版参数列表中,class… 或 typename… 指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟 … 指出接下来表示零个或多个形对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模版一样,每个参数实例化时遵循引用折叠规则。

  1、 基本语法

  • 模板参数包:templateArgs为零或多个类型);
  • 函数参数包:void Func(Args&&… args)(args为零或多个参数);
  • 包扩展:通过触发参数包展开;
  • 参数个数获取:sizeof…(args)(返回参数包中参数的个数)。

  2、示例演示

  1. //==============================可变参数模板==============================
  2. template<class...Args>
  3. void Print(Args&&...args) //万能引用,这样传左值还是右值都可以,编译器会自动推导Args类型
  4. {
  5. cout << sizeof...(args) << endl;
  6. }
  7. int main()
  8. {
  9. double x = 1.1;
  10. Print(); // 包里有0个参数
  11. Print(1); // 包里有1个参数
  12. Print(x, string("xxxxx")); // 包里有2个参数
  13. Print(2.2, string("xxxxx"), x); // 包里有3个参数
  14. return 0;
  15. }
复制代码

  3、参数包展开:编译时递归推导

  • 对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(…)来触发扩展操作

可变参数模板的核心是“包扩展”,通过递归调用实现参数逐个解析(需定义递归终止条件):

  1. //==============================参数包扩展==============================
  2. // 编译时递归推导解析参数
  3. void ShowList()
  4. {
  5. // 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
  6. cout << endl;
  7. }
  8. template<class T, class ...Args>
  9. void ShowList(T x, Args... args)
  10. {
  11. //这是运行时的递归终止,而推导的过程是编译时,所以不能这样写
  12. //if (sizeof..(args) == 0)
  13. //{
  14. // return;
  15. //}
  16. cout << x << " ";
  17. // args是N个参数的参数包
  18. // 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
  19. ShowList(args...);
  20. }
  21. template<class ...Args>
  22. void Print(Args ...args)
  23. {
  24. // 解析参数包,递归推导
  25. ShowList(args...);
  26. }
  27. int main()
  28. {
  29. Print();
  30. Print(1);
  31. Print(1, string("xxxxx"));
  32. Print(1, string("xxxxx"), 2.2);
  33. return 0;
  34. }
复制代码

底层的实现细节如下图所示:

五. 落地实践:emplace 系列接口的实现

      C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能兼容push和insert系列,但是empalce还支持新玩法,假设容器为container,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象

  1、emplace_back 与 push_back 的区别

  • push_back:需先构造临时对象,再通过拷贝 / 移动构造到容器中;
  • emplace_back:接收构造对象的参数包,直接在容器节点中构造对象,减少一次拷贝 / 移动。

string模拟实现(将调用函数进行打印便于观察):

  1. namespace xiaoye
  2. {
  3. class string
  4. {
  5. public:
  6. typedef char* iterator;
  7. typedef const char* const_iterator;
  8. iterator begin()
  9. {
  10. return _str;
  11. }
  12. iterator end()
  13. {
  14. return _str + _size;
  15. }
  16. const_iterator begin() const
  17. {
  18. return _str;
  19. }
  20. const_iterator end() const
  21. {
  22. return _str + _size;
  23. }
  24. string(const char* str = "")
  25. :_size(strlen(str))
  26. , _capacity(_size)
  27. {
  28. cout << "string(char* str)-构造" << endl;
  29. _str = new char[_capacity + 1];
  30. strcpy(_str, str);
  31. }
  32. ~string()
  33. {
  34. //cout << "~string() -- 析构" << endl;
  35. delete[] _str;
  36. _str = nullptr;
  37. _size = _capacity = 0;
  38. }
  39. void swap(string& s)
  40. {
  41. std::swap(_str, s._str);
  42. std::swap(_size, s._size);
  43. std::swap(_capacity, s._capacity);
  44. }
  45. // 拷贝构造
  46. string(const string& s)
  47. {
  48. cout << "string(const string& s) -- 拷贝构造" << endl;
  49. reserve(s._capacity); //拷贝构造需要额外再开辟空间进行拷贝数据
  50. for (auto e : s)
  51. {
  52. push_back(e);
  53. }
  54. }
  55. // 移动构造
  56. string(string&& rs)
  57. {
  58. cout << "string(string&& s) -- 移动构造" << endl;
  59. swap(rs); //移动构造只需要"掠夺"右值引用形参的资源即可实现构造
  60. }
  61. //拷贝赋值
  62. string& operator=(const string& s)
  63. {
  64. cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
  65. if (this != &s)
  66. {
  67. _str[0] = '\n';
  68. _size = 0;
  69. reserve(s._capacity); //拷贝赋值需要额外再开辟空间进行拷贝赋值数据
  70. for (auto e : s)
  71. {
  72. push_back(e);
  73. }
  74. }
  75. return *this;
  76. }
  77. //移动赋值
  78. string& operator=(string&& rs)
  79. {
  80. cout << "string& operator=(string&& s) -- 移动赋值" << endl;
  81. swap(rs); //移动赋值只需要"掠夺"右值引用形参的资源即可实现赋值
  82. //并且不需要手动先删除旧数据,交换完后出了函数形参rs就会带着旧数据一起被销毁
  83. return *this;
  84. }
  85. void reserve(size_t new_capacity)
  86. {
  87. if (new_capacity > _capacity)
  88. {
  89. char* tmp = new char[new_capacity + 1];
  90. if (_str)//_str不为空才能进行delete,否则会报错
  91. {
  92. strcpy(tmp, _str);
  93. delete[]_str;
  94. }
  95. _str = tmp;
  96. _capacity = new_capacity;
  97. }
  98. }
  99. char& operator[](size_t pos)
  100. {
  101. assert(pos < _size);
  102. return _str[pos];
  103. }
  104. void push_back(char ch)
  105. {
  106. if (_size >= _capacity)
  107. {
  108. size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
  109. reserve(newcapacity);
  110. }
  111. _str[_size] = ch;
  112. ++_size;
  113. _str[_size] = '\0';
  114. }
  115. string& operator+=(char ch)
  116. {
  117. push_back(ch);
  118. return *this;
  119. }
  120. const char* c_str() const
  121. {
  122. return _str;
  123. }
  124. size_t size() const
  125. {
  126. return _size;
  127. }
  128. size_t capacity() const
  129. {
  130. return _capacity;
  131. }
  132. private:
  133. char* _str = nullptr;
  134. size_t _size = 0;
  135. size_t _capacity = 0;
  136. };
  137. }
复制代码

实际操作

  • 场景一:基础场景( list ) && 减少一次移动构造
  1. //==============================empalce系列接口==============================
  2. #include<list>
  3. // emplace_back总体而言是更加高效的,推荐以后使用emplace系列替代insert和push系列
  4. int main()
  5. {
  6. list<xiaoye::string> lt;
  7. // 传左值,跟push_back一样,走拷贝构造
  8. xiaoye::string s1("1111111111");
  9. lt.emplace_back(s1);
  10. cout << "************************************" << endl;
  11. // 右值,跟push_back一样,走移动构造
  12. lt.emplace_back(move(s1));
  13. cout << "************************************" << endl;
  14. // 直接把构造string参数包往下传,直接用string参数包构造string
  15. // 这里达到的效果是push_back做不到的
  16. lt.push_back("1111111111");
  17. cout << "************************************" << endl;
  18. lt.emplace_back("1111111111");
  19. cout << "************************************" << endl;
  20. return 0;
  21. }
复制代码

  • 场景二:进阶场景( list / list) && 减少一次拷贝构造
  1. #include<list>
  2. struct Date
  3. {
  4. public:
  5. Date(int y, int m, int d)
  6. :_y(y)
  7. , _m(m)
  8. , _d(d)
  9. {
  10. cout << "Date() 构造" << endl;
  11. }
  12. Date(const Date& d)
  13. :_y(d._y)
  14. , _m(d._m)
  15. , _d(d._d)
  16. {
  17. cout << "Date(const Date& d) 拷贝构造" << endl;
  18. }
  19. Date(Date&& d)
  20. :_y(d._y)
  21. , _m(d._m)
  22. , _d(d._d)
  23. {
  24. cout << "Date(Date&& d) 移动构造" << endl;
  25. }
  26. private:
  27. int _y = 1;
  28. int _m = 1;
  29. int _d = 1;
  30. };
  31. int main()
  32. {
  33. list<pair<xiaoye::string, int>> lt1;
  34. // 跟push_back一样
  35. // 构造pair + 拷贝/移动构造pair到list的节点中data上
  36. pair<xiaoye::string, int> kv("苹果", 1);
  37. lt1.emplace_back(kv);
  38. cout << "****************************" << endl;
  39. // 跟push_back一样
  40. lt1.emplace_back(move(kv));
  41. cout << "****************************" << endl;
  42. // 直接把构造pair参数包往下传,直接用pair参数包构造pair
  43. // 这里达到的效果是push_back做不到的
  44. lt1.emplace_back("苹果", 1);
  45. //lt1.emplace_back({ "苹果", 1 });
  46. //这样传是错误的,原因我们需要结合{}来解释:
  47. //首先我们要知道为什么 { } 可以在push_back中使用?
  48. //因为push_back中已经确定了函数参数的类型就是pair,所以传入的 {} 可以进行隐式类型转换成pair
  49. //而emplace_back是一个可变参数的函数模板,而这个可变模板参数Args只有确定传入的数据类型才会进行推导类型
  50. //而 {} 又必须要知道函数参数类型是什么才能进行隐式类型转换,导致两边都无法进行
  51. //那为什么直接传入两个独立数据就可以呢?原因就在于直接传入两个独立数据,
  52. //Arges就可以推导成int, int,那么就可以调用pair的构造函数pair (const first_type& a, const second_type& b);
  53. cout << "****************************" << endl;
  54. //lt1.push_back("苹果", 1);
  55. // error,要传 pair 或者 {}进行隐式类型转换成pair的值
  56. lt1.push_back({ "苹果", 1 });
  57. cout << "****************************" << endl;
  58. list<Date> lt;
  59. // 构造 + 拷贝构造
  60. Date d1{ 2026,4,14 };
  61. lt.push_back(d1);
  62. cout << "************************************" << endl;
  63. // 构造(临时对象) + 移动构造
  64. lt.push_back({ 2026,4,14 });
  65. cout << "************************************" << endl;
  66. // 传构造Dtae的参数,传给形参参数包,参数包往下不断传递,最后直接构造到链表节点上
  67. // 直接构造
  68. lt.emplace_back(2026,4,14);
  69. //lt.emplace_back({ 2026,4,14 });//error,同理和上面的pair的原因一样
  70. return 0;
  71. }
复制代码

  2、模拟实现 list 的 emplace_back

关键代码

  1. namespace xiaoye
  2. {
  3. //链表结点
  4. template<class T>
  5. struct ListNode
  6. {
  7. ListNode<T>* _next;
  8. ListNode<T>* _prev;
  9. T _data;
  10. ListNode(T&& data)
  11. :_next(nullptr)
  12. , _prev(nullptr)
  13. , _data(move(data)) // 移动构造或拷贝构造
  14. {
  15. }
  16. template<class... Args>
  17. ListNode(Args&&... args)
  18. : _next(nullptr)
  19. , _prev(nullptr)
  20. , _data(std::forward<Args>(args)...)
  21. {
  22. }
  23. };
  24. //链表迭代器
  25. template<class T, class Ref, class Ptr>
  26. struct ListIterator
  27. {
  28. typedef ListNode<T> Node;
  29. typedef ListIterator<T, Ref, Ptr> Self;
  30. Node* _node;
  31. ListIterator(Node* node)
  32. :_node(node)
  33. {
  34. }
  35. Self& operator++()
  36. {
  37. _node = _node->_next;
  38. return *this;
  39. }
  40. Ref operator*()
  41. {
  42. return _node->_data;
  43. }
  44. Ptr operator->()
  45. {
  46. return &_node->_data;
  47. }
  48. bool operator!=(const Self& it)
  49. {
  50. return _node != it._node;
  51. }
  52. };
  53. //链表
  54. template<class T>
  55. class list
  56. {
  57. typedef ListNode<T> Node;
  58. public:
  59. typedef ListIterator<T, T&, T*> iterator;
  60. typedef ListIterator<T, const T&, const T*> const_iterator;
  61. iterator begin()
  62. {
  63. return iterator(_head->_next);
  64. }
  65. iterator end()
  66. {
  67. return iterator(_head);
  68. }
  69. list()
  70. {
  71. _head = new Node();
  72. _head->_next = _head;
  73. _head->_prev = _head;
  74. }
  75. //左值引用版本的尾插
  76. void push_back(const T& x)
  77. {
  78. insert(end(), x);
  79. }
  80. //右值引用版本的尾插
  81. void push_back(T&& x)
  82. {
  83. insert(end(), forward<T>(x));
  84. }
  85. template<class...Args>
  86. void emplace_back(Args&&...args)
  87. {
  88. insert(end(), std::forward<Args>(args)...);
  89. }
  90. // 原理:本质编译器根据可变参数模板生成对应参数的函数
  91. /*void emplace_back(string& s)
  92. {
  93. insert(end(), std::forward<string>(s));
  94. }
  95. void emplace_back(string&& s)
  96. {
  97. insert(end(), std::forward<string>(s));
  98. }
  99. void emplace_back(const char* s)
  100. {
  101. insert(end(), std::forward<const char*>(s));//这个就会直接调用string的构造函数
  102. }
  103. */
  104. //左值引用版本的插入
  105. iterator insert(iterator pos, const T& x)
  106. {
  107. Node* cur = pos._node;
  108. Node* newnode = new Node(x);
  109. Node* prev = cur->_prev;
  110. // prev newnode cur
  111. prev->_next = newnode;
  112. newnode->_prev = prev;
  113. newnode->_next = cur;
  114. cur->_prev = newnode;
  115. return iterator(newnode);
  116. }
  117. //右值引用版本的插入
  118. iterator insert(iterator pos, T && x)
  119. {
  120. Node* cur = pos._node;
  121. Node* newnode = new Node(forward<T>(x));
  122. Node* prev = cur->_prev;
  123. // prev newnode cur
  124. prev->_next = newnode;
  125. newnode->_prev = prev;
  126. newnode->_next = cur;
  127. cur->_prev = newnode;
  128. return iterator(newnode);
  129. }
  130. template<class... Args>
  131. iterator insert(iterator pos, Args&&... args)
  132. {
  133. Node* cur = pos._node;
  134. Node* newnode = new Node(std::forward<Args>(args)...);
  135. Node* prev = cur->_prev;
  136. // prev newnode cur
  137. prev->_next = newnode;
  138. newnode->_prev = prev;
  139. newnode->_next = cur;
  140. cur->_prev = newnode;
  141. return iterator(newnode);
  142. }
  143. private:
  144. Node* _head;
  145. };
  146. }
复制代码

测试代码

  1. //==============================模拟实现 list 的 emplace_back==============================
  2. int main()
  3. {
  4. xiaoye::list<xiaoye::string> lt;
  5. xiaoye::string s1("111111111111");
  6. lt.emplace_back(s1);
  7. cout << "*********************************" << endl;
  8. lt.emplace_back(move(s1));
  9. cout << "*********************************" << endl;
  10. lt.emplace_back("111111111111");
  11. cout << "*********************************" << endl;
  12. xiaoye::list<pair<xiaoye::string, int>> lt1;
  13. pair<xiaoye::string, int> kv("苹果", 1);
  14. lt1.emplace_back(kv);
  15. cout << "*********************************" << endl;
  16. lt1.emplace_back(move(kv));
  17. cout << "*********************************" << endl;
  18. ////////////////////////////////////////////////////////////////////
  19. lt1.emplace_back("苹果", 1);
  20. cout << "*********************************" << endl;
  21. return 0;
  22. }
复制代码

结束语

      到此,C++11的引用折叠、完美转发与可变参数模板就讲解完了。引用折叠、完美转发和可变参数模板是 C++11 泛型编程的 “三剑客”,看似抽象,但落地场景十分具体 — emplace 接口、万能引用、函数参数转发等高频用法都基于这些特性。。实际开发中,建议优先使用emplace_back 替代 push_back,合理运用万能引用和完美转发简化代码,同时注意值类别的区分,避免因转发错误导致性能的无端损耗。希望对大家学习C++能有所收获!

C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2026 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行