人类发展史,就是不断挖坑、填坑的过程。
语言发展史也是如此!
任何一门设计合理的语言,给你的限制或提供的什么特性,都不是没有代价的。
C的指针
指针:pointer
指针的思想起源于汇编。指针思想是编程思想历史上的重大飞跃。
每一个编程语言都使用指针。C语言将指针完全暴露给了用户。潘多拉之盒。
使用指针的必要性:资源管理,即地址管理。
思想层:将地址包了一层。
语法层:T *p; *p;
编译器:包含一个intptr_t类型成员的结构体。
汇编层:寄存器间接寻址MOV。
C语言中只有一种参数传递方式:值传递。
void f(int p)
void f(int *p)
利用指针交换两个数字
#include <stdio.h>
void Swap(int *p1,int *p2){
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int main(){
int a = 10;
int b = 20;
Swap(&a,&b);
printf("%d %d\n",a,b);
return 0;
}
指针的级数
Int *p; int **p; int ***p;
理论上无限级,无限套娃。实际上受编译器限制。
指针是一扇门,推开门,后面是整个世界。
C++的引用
引用:reference
已存在变量的别名。
使用引用的必要性:资源使用
思想层:受限制的指针。
语法层:T a; T &p=a;
编译器:给你做了保证,一定是经过初始化的指针
汇编层:和指针一样。
在汇编层,指针和引用是完全一样的。
引用是一个语法糖,T a; T &p=a;
等价于 T *const p = &a
int x=0;
00676664 mov dword ptr [x],0
int &a = x;
0067666B lea eax,[x]
0067666E mov dword ptr [a],eax
a = 1;
00676671 mov eax,dword ptr [a]
00676674 mov dword ptr [eax],1
int *p = &x;
0067667A lea eax,[x]
0067667D mov dword ptr [p],eax
*p = 2;
00676680 mov eax,dword ptr [p]
00676683 mov dword ptr [eax],2
int *const p2 = &x;
00676689 lea eax,[x]
0067668C mov dword ptr [p2],eax
*p2 = 3;
0067668F mov eax,dword ptr [p2]
00676692 mov dword ptr [eax],3
引用的情况:
int a = 1;
const int b = 1;
int &ref1 = a;
int &ref2 = 1;//ERROR
const int &ref3 = b;
const int &ref4 = 1;
Q:唯独int &ref2 = 1;//ERROR?
A:C++的早期这种语法是被允许的,但是在函数调用传参数时,会给程序员带来误解,于是后面就禁止了这种语法。
引用规则的特例:const引用
void f(int &i){}
void f(const int &i){}
int main(){
int i = 1;
f(i);//call f(int &i)
f(2);//call f(const int &i)
return 0;
}
void f(int &i){}
//void f(const int &i){}
int main(){
int i = 1;
f(i);//call f(int &i)
f(2);//ERROR
return 0;
}
//void f(int &i){}
void f(const int &i){}
int main(){
int i = 1;
f(i);//call f(const int &i)
f(2);//call f(const int &i)
return 0;
}
C++语言中就有了新的参数传递方式:引用传递 void f(T &p) 。实质也是传值。
自定义类型最好用引用传递,可以避免不必要的构造函数和析构函数的调用。
内置类型建议用值传递,自定义类型建议用引用传递,内置类型,值传递会比按引用传递更高效。
解释见:这里
利用引用交换两个数字
#include <iostream>
#include <stdlib.h>
using namespace std;
void swap(int &a, int &b){
int tmp = a;
a = b;
b = tmp;
}
int main(){
int a = 3;
int b = 4;
swap(a, b);
cout << "a=" << a<<" " << "b=" << b << endl;
return 0;
}
引用的级数
只能一级,引用的对象必须是一个已存在的地址。引用变量本身的地址,外界不能访问。
References are not objects; they do not necessarily occupy storage,
Because references are not objects, there are no arrays of references, no pointers to references, and no references to references。
int& a[3]; // ERROR
int&* p; // ERROR
int& &r; // ERROR
引用和指针叠加
int a; int *p = &a; int *&r = p; //OK
使用引用的场景:
- 给函数传递可变参数
- 给函数传递大型对象
- 引用函数返回值;
Q:引用能实现的基本上指针都可以实现,那为什么C++还需要引入引用呢?
A:最初主要是为了支持运算符重载。
c = a + b是可以接受的写法,而c = &a + &b 就不是很方便而且有歧义了。
写法上的方便是要第一考虑的。
Q:C++引入了引用,那为什么C++不和Java一样让指针对使用者不可见呢?
A:历史原因。为了兼容C语言。程序员是自由的。
Q:C++为什么选择&作为引用的标识符?
A:需要用一个符号告诉编译器,传的是引用。&在C语言中是取地址的作用,于是就选择了它。
Q:this为什么是指针类型,而不是引用类型?
A:历史原因。this诞生早于引用。某种意义上来讲,this应该被设计为引用类型。
Q:Why is “this” not a reference?
A:Because “this” was introduced into C++ (really into C with Classes) before references were added. Also, I chose “this” to follow Simula usage, rather than the (later) Smalltalk use of “self”.
Q:拷贝构造函数参数一定是引用,不然编译通不过,为什么?
A:因为在入参的过程中,如果不是引用,会首先进行一次值拷贝;而要实现的就是拷贝构造,就会造成不断的递归最后爆炸。
Q:引用是受限制的指针,哪里受限制了?
A:引用变量本身的地址外界不可获得,当然编译器是可以的。
Q:引用变量是否占有内存空间?
A:引用可以不占用,也可以占有。语法规定对引用变量的操作都是对被引用对象的操作。
struct S {
int a;
int &b;
};
int x = 123;
S s(123,x);
sizeof(S)=?//32位环境等于8
non-const引用的汇编视角
const引用的汇编视角
说明:const引用变量绑定没有地址的对象时,会生成一个临时变量/匿名对象来中转。
全局定义
const int& a = 123; 123的匿名对象在堆上
局部定义
void f{
const int& a = 456; 456的匿名对象在栈上
}
往下走用*,往上走用&。
C++的第一个坑:兼容了C语言的指针。
C++的构造
3种构造语义:
- 构造函数constructor
- 拷贝构造copy constructor
- 拷贝赋值运算符copy assignment operator
构造函数S()
出厂设置
拷贝构造S(const S &other)
把A的数据复制给B。B(A);
拷贝赋值运算符S& operator=(const S &other)
先把B的资源释放,再把A的数据复制给B。B=A;
变量按内存分为两种:
- 不包含指针。trivial type。篮球。
- 包含指针。handle type。风筝和风筝线。
拷贝的分类
- 浅拷贝
引用语意(reference semantics)
缺陷:若写法不对,可能会发生double free。
Q:为什么编译器所有的默认的行为都是浅拷贝?
A:深拷贝不一定能实现。指向的对象可能是多态的,也可能是数组,也可能有循环引用。所以只能留待成员变量的类来决定怎样实现复制。
有时候为了防止默认拷贝发生,可以声明一个私有的拷贝构造函数。这种做法比较常见,但不可取。 - 深拷贝
值语意(value semantics)
缺陷:出现了额外的构造和析构,性能损失。
深拷贝和浅拷贝的本质区别就是两个对象的行为属性是否是独立变化的。
C++的第二个坑:拷贝构造函数
思考:
T为handle type,T A(…),T B(A)。A赋值给B。如果A不再使用了,能不能让B直接接管A的所有资源呢?(移动语义move semantics)
在不破坏现有语法规则情况下,你会如何设计?
- C++03现有语法不支持移动语义。需要新增移动语义。
- 如何标识对象的资源是可以被移动的呢?
- 这种机制必须以一种最低开销的方式实现,并且对所有的类都有效。
- 设计在编译层,与运行层面无关。
C++的设计者们注意到,大多数情况下,右值所包含的对象都是可以安全的被移动的。
左值与右值
左值和右值的概念
CPL语言引入了表达式值的类型value categories这种概念:左值和右值,left or right of assignment。
C语言沿用了类似的分类:左值和其他,locator value and other
C++98 沿用了C语言的分类,但是略微调整。引入了新定义:右值rvalue = non-lvalue。
C++11 新增了xvalue(an “eXpiring” value),并调整了之前左值和右值的定义。
(i)has identity: it’s possible to determine whether the expression refers to the same entity as another expression, such as by comparing addresses of the objects or the functions they identify (obtained directly or indirectly);
(m)can be moved from: move constructor, move assignment operator, or another function overload that implements move semantics can bind to the expression.
准则 1:能不能分辨两个表达式指的是同一个物体。比如我们可以通过比较地址。
准则 2:能不能使用移动语义。比如看看能不能用调用移动构造函数。
- i&~m:lvalue 左值
- i&m:xvalue 将亡值
- ~i&m:prvalue 纯右值
- i:glvalue泛左值
- m:rvalue右值
C++17
分类和 C++11 是一样的,但是语义上更加明确了。
- glvalues:有自己地址的长寿对象
- prvalues:为了初始化而用的短命对象
- xvalue:资源已经不需要了,而且可以再利用的长寿对象
为了优化这样一个情况:T(T(T(x)))==>T(x),将prvalues的定义略微调整了下。
具体可以参考Copy elision (复制消除)
左值引用和右值引用
T &Lref; // 左值引用,就是传统的c++引用
T &&Rref; // 右值引用
Q:为什么使用&&做为右值引用的标识符?
A:惯性设计。标准委员玩标点符号是真的可以。
规则:
- non-const左值引用只能绑定non-const左值
- non-const右值引用只能绑定non-const右值
- const左值引用,可以绑定任意。
- const右值引用,可以绑定non-const右值和const右值。注:这个使用的场景很少很少。
如何判定
namespace test {
template <typename T> struct is_lvalue_reference {
const static bool value = false;
};
template <typename T> struct is_lvalue_reference<T &> {
const static bool value = true;
};
template <typename T> struct is_rvalue_reference {
const static bool value = false;
};
template <typename T> struct is_rvalue_reference<T &&> {
const static bool value = true;
};
template <typename T> struct is_lvalue {
const static bool value = is_lvalue_reference<T>::value && (!is_rvalue_reference<T>::value);
};
template <typename T> struct is_xvalue {
const static bool value = (!is_lvalue_reference<T>::value) && is_rvalue_reference<T>::value;
};
template <typename T> struct is_prvalue {
const static bool value = (!is_lvalue_reference<T>::value && !is_rvalue_reference<T>::value);
};
template <typename T> struct is_rvalue {
const static bool value = (is_xvalue<T>::value || is_prvalue<T>::value);
};
template <typename T> struct is_glvalue {
const static bool value = (is_xvalue<T>::value || is_lvalue<T>::value);
};
}
struct Foo {};
Foo funRetFoo();
Foo &funRetFooLRef();
Foo &&funRetFooRRef();
TEST(TypeTraits, isRvalue) {
//base type
EXPECT_FALSE(::test::is_lvalue_reference<int>::value);
EXPECT_FALSE(::test::is_rvalue_reference<int>::value);
EXPECT_FALSE(::test::is_lvalue<int>::value);
EXPECT_FALSE(::test::is_xvalue<int>::value);
EXPECT_TRUE(::test::is_prvalue<int>::value);
EXPECT_FALSE(::test::is_glvalue<int>::value);
EXPECT_TRUE(::test::is_rvalue<int>::value);
// return obj
EXPECT_FALSE(::test::is_lvalue_reference<decltype(funRetFoo())>::value);
EXPECT_FALSE(::test::is_rvalue_reference<decltype(funRetFoo())>::value);
EXPECT_FALSE(::test::is_lvalue<decltype(funRetFoo())>::value);
EXPECT_FALSE(::test::is_xvalue<decltype(funRetFoo())>::value);
EXPECT_TRUE(::test::is_prvalue<decltype(funRetFoo())>::value);
EXPECT_FALSE(::test::is_glvalue<decltype(funRetFoo())>::value);
EXPECT_TRUE(::test::is_rvalue<decltype(funRetFoo())>::value);
// return ref obj
EXPECT_TRUE(::test::is_lvalue_reference<decltype(funRetFooLRef())>::value);
EXPECT_FALSE(::test::is_rvalue_reference<decltype(funRetFooLRef())>::value);
EXPECT_TRUE(::test::is_lvalue<decltype(funRetFooLRef())>::value);
EXPECT_FALSE(::test::is_xvalue<decltype(funRetFooLRef())>::value);
EXPECT_FALSE(::test::is_prvalue<decltype(funRetFooLRef())>::value);
EXPECT_TRUE(::test::is_glvalue<decltype(funRetFooLRef())>::value);
EXPECT_FALSE(::test::is_rvalue<decltype(funRetFooLRef())>::value);
// return rref obj
EXPECT_FALSE(::test::is_lvalue_reference<decltype(funRetFooRRef())>::value);
EXPECT_TRUE(::test::is_rvalue_reference<decltype(funRetFooRRef())>::value);
EXPECT_FALSE(::test::is_lvalue<decltype(funRetFooRRef())>::value);
EXPECT_TRUE(::test::is_xvalue<decltype(funRetFooRRef())>::value);
EXPECT_FALSE(::test::is_prvalue<decltype(funRetFooRRef())>::value);
EXPECT_TRUE(::test::is_glvalue<decltype(funRetFooRRef())>::value);
EXPECT_TRUE(::test::is_rvalue<decltype(funRetFooRRef())>::value);
int lvalue;
// 模拟=号左边
EXPECT_TRUE(::test::is_lvalue_reference<decltype(*&lvalue)>::value);
EXPECT_FALSE(::test::is_rvalue_reference<decltype(*&lvalue)>::value);
EXPECT_TRUE(::test::is_lvalue<decltype(*&lvalue)>::value);
EXPECT_FALSE(::test::is_xvalue<decltype(*&lvalue)>::value);
EXPECT_FALSE(::test::is_prvalue<decltype(*&lvalue)>::value);
EXPECT_TRUE(::test::is_glvalue<decltype(*&lvalue)>::value);
EXPECT_FALSE(::test::is_rvalue<decltype(*&lvalue)>::value);
//operator++()
EXPECT_FALSE(::test::is_lvalue_reference<decltype(lvalue++)>::value);
EXPECT_FALSE(::test::is_rvalue_reference<decltype(lvalue++)>::value);
EXPECT_FALSE(::test::is_lvalue<decltype(lvalue++)>::value);
EXPECT_FALSE(::test::is_xvalue<decltype(lvalue++)>::value);
EXPECT_TRUE(::test::is_prvalue<decltype(lvalue++)>::value);
EXPECT_FALSE(::test::is_glvalue<decltype(lvalue++)>::value);
EXPECT_TRUE(::test::is_rvalue<decltype(lvalue++)>::value);
//operator++(int)
EXPECT_TRUE(::test::is_lvalue_reference<decltype(++lvalue)>::value);
EXPECT_FALSE(::test::is_rvalue_reference<decltype(++lvalue)>::value);
EXPECT_TRUE(::test::is_lvalue<decltype(++lvalue)>::value);
EXPECT_FALSE(::test::is_xvalue<decltype(++lvalue)>::value);
EXPECT_FALSE(::test::is_prvalue<decltype(++lvalue)>::value);
EXPECT_TRUE(::test::is_glvalue<decltype(++lvalue)>::value);
EXPECT_FALSE(::test::is_rvalue<decltype(++lvalue)>::value);
}
记住一点:左值引用直接作用于lvalue,右值引用直接作用于xvalue。
Q:谁直接作用于prvalue呢?
A:只能间接作用:右值引用、const左值引用。生成一个指针类型的匿名变量做中转。
移动语义
如何让自定义对象支持移动语义?
- 移动构造move constructorS(S &&other) noexcept
- 移动赋值运算符move assignment operator S& operator=(S &&other) noexcept
note:需要加noexpect。
目的:告诉编译器:这个移动函数不会抛出异常,可以放心地调用。否则,编译器会调用拷贝函数。
C++11后,STL都支持了移动语义。移动语义对基本类型没有性能提升的作用。
Q:如何将一个变量转为右值引用?
A:static_cast<T &&>(t)
写法一:
string s = "abcd";
string s1(static_cast<string &&>(s));
s1(s);
string s2 = static_cast<string &&>(s);
这种写法麻烦。如何简写?模板。
写法二:
//VS
template <class _Ty>
struct remove_reference<_Ty&&> {
using type = _Ty;
using _Const_thru_ref_type = const _Ty&&;
};
template <class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;
// FUNCTION TEMPLATE move
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
string s = "abcd";
string s1(move(s));
string s2 = move(s);
由此诞生了std::move。在<utility>
头文件中。
利用移动交换两个数字
void swap(T &a1, T &a2){
T tmp(std::move(a1)); // a1 转为右值,移动构造函数调用,低成本
a1 = std::move(a2); // a2 转为右值,移动赋值函数调用,低成本
a2 = std::move(tmp); // tmp 转为右值移动给a2
}
Q:move的参数是&&,为什么传入左值也是可以的?
A:万能引用
A=B B的资源给A了,A的资源自己处理掉的。如果B在外面继续使用,则是未定义行为。
万能引用
universal reference
概念:使用T&&类型的形参既能绑定右值,又能绑定左值。
T&&在代码里并不总是右值引用。
万能引用一定涉及到类型推导的,没有类型推导就是右值引用。
具体规则:这篇文章
使用场景有2个:
template<typename T>
void f(T&& param); // param is a universal reference
auto&& var2 = var1; // var2 is a universal reference
引用折叠规则
reference-collapsing rules
引入该规则的原因:C++中禁止reference to reference。为了通过编译器检查。
当左值引用和右值引用同时出现在类型定义中时,需要如何处理?
约定:只有&& && = &&,沾上一个&就变左值引用了。
T && && ==>T &&
T && & ==>T &
T & && ==>T &
T & & ==>T &
Q:为什么要这么约定?
A:左值引用有副作用
函数内外参数类型不匹配
template<typename T>
void f(T&& a){
g(a); // 这里的 a 是什么类型?
}
// 版本 1
template<typename T>
void g(T &){ cout << "T&" << endl; }
// 版本 2
template<typename T>
void g(T &&){ cout << "T&&" << endl; }
int num;
f(0);
f(num);
输出:
T&
T&
a是变量,是左值,所以输出T&。
但是0是数字,是右值,为什么进去后就成了左值?能不能一致?
Q:一定要一致么?
A:在某些场景不一致会有问题。需要提供一种保证前后语义一致的机制。
Q:怎么才能实现一致呢?
A:还是static_cast。
template<typename T>
void f(T&& a){
g(static_cast<T &&>(a));
//g(static_cast<T>(a)); 这样写也可以
}
// 版本 1
template<typename T>
void g(T &){ cout << "T&" << endl; }
// 版本 2
template<typename T>
void g(T &&){ cout << "T&&" << endl; }
int a;
f(0);
f(a);
输出:
T&&
T&
这种写法麻烦。如何简写?模板。
Forward
转发:某些函数需要将其中一个或多个实参连同类型不变地转发给其他函数
//VS
// FUNCTION TEMPLATE forward
template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg);
}
template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvalue
static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
return static_cast<_Ty&&>(_Arg);
}
就可以简写:
template<typename T>
void f(T&& a){
g(forward<T>(a));
}
好像也没啥好处,就是static_cast替换为了forward。
区别:
- static_cast 关键字
- std::forward 模板函数,支持函数模板的变长参数。在
<utility>
头文件中。
例子:make_unique
// FUNCTION TEMPLATE make_unique
template <class _Ty, class... _Types, enable_if_t<!is_array_v<_Ty>, int> = 0>
_NODISCARD unique_ptr<_Ty> make_unique(_Types&&... _Args) { // make a unique_ptr
return unique_ptr<_Ty>(new _Ty(_STD forward<_Types>(_Args)...));
}
template <class _Ty, enable_if_t<is_array_v<_Ty> && extent_v<_Ty> == 0, int> = 0>
_NODISCARD unique_ptr<_Ty> make_unique(size_t _Size) { // make a unique_ptr
using _Elem = remove_extent_t<_Ty>;
return unique_ptr<_Ty>(new _Elem[_Size]());
}
Perfect forward
转发时,需要保持被转发实参的所有性质不变:是否const、以及是左值还是右值。
完美转发 = std::forward + 万能引用 + 引用折叠
用途:一般作为多参数函数调用的中间层。
struct A{
A(int &&n){ cout << "rvalue overload, n=" << n << endl; }
A(int &n){ cout << "lvalue overload, n=" << n << endl; }
A(const int &&n){ cout << "rvalue const overload, n=" << n << endl; }
A(const int &n){ cout << "lvalue const overload, n=" << n << endl; }
};
class B{
public:
template<typename T1, typename T2, typename T3, typename T4>
B(T1 &&t1, T2 &&t2, T3 &&t3, T4 &&t4) :
a1_(std::forward<T1>(t1)),
a2_(std::forward<T2>(t2)),
a3_(std::forward<T3>(t3)),
a4_(std::forward<T4>(t4)) {}
private:
A a1_, a2_, a3_, a4_;
};
int main(){
int i = 1, j = 2, k = 3, m = 4;
int &i_lref = i;
const int &j_clref = j;
int &&k_rref = std::move(k);
const int &&m_crref = std::move(m);
B b1(1, 2, 3, 4);
B b2(i, j, k, m);
B b3(i_lref, j_clref, k_rref, m_crref);
B b4(i_lref, j_clref, std::move(k), static_cast<const int &&>(m));
return 0;
}
直观的感受:构造函数只用写一个,就可以满足所有情形。代替了4^4=256种重载形式。
perfect的由来:参数传递的七种方案
不支持转发的场景 EffectiveModernCppChinese/item30.md
汇编视角
第一个视角
右值引用就是一个指向一个匿名变量的指针**。右值引用就是给了外界接触这个匿名变量的机会。**
可见初始化一个右值引用其实是开辟了两块空间,一块是右值引用类型那么大的匿名变量,一块是指向这个匿名变量的指针。
第二个视角
修改右值引用值的过程也分为两步,取出指针的值,也就是匿名变量的地址,把右值赋值给地址所指的匿名变量。和修改指针的值一样的。
Q:如何理解“右值引用延长了右值的生命周期”?
A:右值被放到了一个变量里,已经拥有了内存,当然就避免了被从寄存器里扔出去就消失的命运。这一块内存的生命周期就和右值引用变量一致了。
最佳实践
网上找的一个图:
资源使用语义
老师给大宝一个球,大宝拿去玩。老师说:注意些别玩坏了。
球的状态:可变:non-const,不可变:const。
意图的体现。
大宝在玩球,小宝也想玩,怎么办?
1. 小宝加入他,一起玩。
2. 大宝不给,小宝买了一个一模一样的球,各玩各的。
3. 大宝说:我不玩了,给你玩吧。小宝接着玩。
需要三个语意来表达(实际上也足够了):
1. 别名(二者唯一)link
2. 复制(二者各一)CTRL+C、CTRL+V。
3. 移动(一有一无)CTRL+X、CTRL+V。
别名对应引用,复制对应资源复制,移动对应资源转移。
性能的体现。
C++实现
别名语义:reference
复制语义:copy constructor、copy assignment operator。
移动语义:move constructor、move assignment operator。
底层实现都是依赖指针。
设计原则
C++语言设计规则 摘自《C++语言的设计和演化》
"资源使用"上体现的原则:
- 全局平衡
- C++ 的发展必须由实际问题推动
- 对不用的东西不需要付出任何代价(零开销规则)
简单的东西依然保持简单。
总结
C++的两个坑:1.兼容了C语言的指针 2.拷贝构造函数
引用解决了资源使用问题,但无法解决资源管理的问题。
指针是一个潘多拉盒子,要想解决根本问题,只能把盒子抛掉。这个对于C++来说不可能。
但是,C++在如何方便程序员做资源管理这个方向上也做了很多尝试。
开放问题
Q:“指针思想”是所有编程语言的基石,为什么?
A:
参考资料
https://blog.csdn.net/l477918269/article/details/90233908
https://zhuanlan.zhihu.com/p/374392832
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1385.htm
https://en.cppreference.com/w/cpp/language/rule_of_three
https://stackoverflow.com/questions/3582001/what-are-the-main-purposes-of-using-stdforward-and-which-problems-it-solves
https://www.zhihu.com/question/363686723/answer/1910830503
https://blog.csdn.net/qq_33113661/article/details/89040579?
https://zhuanlan.zhihu.com/p/265778316
https://stackoverflow.com/questions/3582001/what-are-the-main-purposes-of-using-stdforward-and-which-problems-it-solves
https://www.cnblogs.com/xusd-null/p/3761637.html
https://zhuanlan.zhihu.com/p/265815272