右值引用的学习
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
左值引用和右值引用
在讲之前,我们先来看一下什么是左值和右值
左值和左值引用
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋
值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左
值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int main()
{// p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;// 以下几个是对上面左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;return 0;
}
右值和右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引
用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址。右值引用就是对右值的引用,给右值取别名。由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。通常情况下,我们只能是从右值表达式获得其引用。比如:
T && a = ReturnRvalue();
这个表达式中,假设ReturnRvalue
返回一个右值,我们就声明了一个名为a
的右值引用,其值等于ReturnRvalue
函数返回的临时变量的值。ReturnRvalue
函数返回的右值在表达式语句结束后,其生命也就终结了,而通过右值引用的声明,该右值又“重获新生”,其生命周期将于右值引用类型变量a的生命周期一样。只要a还“活着”,该右值临时量将会一直“存活”下去。
所以相比于以下语句的声明方式:
T b = ReturnRvalue();
我们刚才的右值引用变量声明,就会少一次对象的析构及一次对象的构造。因为a是右值引用,直接绑定了ReturnRvalue
返回的临时量,而b只是由临时值构造而成的,而临时量在表达式结束后会析构应而就会多一次析构和构造的开销。
double fmin(double x, double y)
{return x + y;
}int main()
{// 以下几个都是常见的右值double x = 1.1, y = 2.2; // 字母常量10; x + y; // 表达式返回值double ret = fmin(x, y); // 函数返回值// 以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);// 这里编译会报错:error C2106: “=”: 左操作数必须为左值/*10 = 1;x + y = 1;fmin(x, y) = 1;*/return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。
左值引用与右值引用比较
先来回顾一下左值引用
int main()
{// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra1 = a; // ra1是a的别名//int& ra2 = 10; // 编译失败,因为10是右值return 0;
}
我们思考一下下面2个问题:
左值引用可以引用右值吗?
右值引用可以引用左值吗?
通过上面的代码我们可以发现左值引用不能引用右值,那么为什么还要问这个问题了?其实我们可以引用右值,因为我们的测试还不够全面。可以看下面的情况:
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
常量左值
为什么加了const之后,就能引用右值了呢?这是因为字符常量具有常性,也就是不能被修改,如果我们使用引用来引用常量,那么我们就能通过引用来修改这个常量,这就违反了常量的特性。简单来说这是一个权限的放大,即“不能被修改变成了可以被修改”,所以加个const引用来限制。
在C++98标准中常量左值就是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。而且在使用右值对其初始化的时候,常量左值引用还可以像右值引用一样将右值的生命期延迟。不过相比于右值引用所引用的右值,常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。看下面的代码。
// 常量左值引用
struct Copyable
{Copyable(){}Copyable(const Copyable& o){cout << "Copied" << endl;}
};Copyable ReturnRvalue() { return Copyable(); }
void AcceptVal(Copyable){}
void AcceptRef(const Copyable &){}int main()
{cout << "Pass by value: " << endl;AcceptVal(ReturnRvalue()); // 临时值被拷贝传入cout << "Pass by reference: " << endl;AcceptRef(ReturnRvalue()); // 临时值被作为引用传递
}
我们声明了一个结构体Copyable
,该结构体的作用是在被拷贝到时候打印一句话Copied
。而两个函数AcceptVal
使用了值传递参数,而AcceptRef
使用了引用传递。在以ReturnRvalue
返回的右值为参数的时候,AcceptRef
就可以直接使用产生的临时值(并延长生命周期),而AcceptVal
则不能直接使用临时对象。
运行代码,结果如下:
Pass by value:
Copied
Copied
Pass by reference:
Copied
可以看到,由于使用了左值引用,临时对象被直接作为函数的参数,而不需要从中拷贝一次。在C++11中,同样的,可以使用以右值引用为参数声明如下函数void AcceptRvalueRef(Copyable &&) {}
也同样可以减少临时变量拷贝到开销。进一步地,还可以再AcceptRvalueRef
中修改该临时值(这个时候临时值由于被右值引用参数所引用,已经获得了函数时间的生命期)。不过修改一个临时值的意义通常不大。
在下表中,列出了在C++11中各种引用类型可以引用的值的类型。需要注意的是,只要能够绑定右值的引用类型,都能够延长右值的生命期。
[!note] 总结
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
接下来看右值引用
int main()
{// 右值引用只能右值,不能引用左值。int&& r1 = 10;// error C2440: “初始化”: 无法从“int”转换为“int &&”// message : 无法将左值绑定到右值引用int a = 10;int&& r2 = a; // 报错// 右值引用可以引用move以后的左值int&& r3 = std::move(a);return 0;
}
这里的move
函数是C++11之后新出的一个函数,其作用是将一个左值强制转换成一个右值,类似强制类型转换,还有move
并不会改变一个变量本身的左值属性,例如 int b = 1;double a = (double)b
这句代码,本质上b还是一个整型类型,只是在这个表达式中,返回了一个double类型的b。
[!note] 总结
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
右值引用的场景和意义
既然我们要知道右值引用的意义是什么,那么就需要先知道左值引用的优点和缺点是什么,有哪些场景要用到左值引用。
- 左值引用降低了内存使用和提高效率
string add_string(string& s1, string& s2)
{string s = s1 + s2;return s;
}int main()
{string str;string hello = "Hello";string world = "world";str = add_string(hello, world);return 0;
}
我们先来回顾一下非引用传参和引用传参的本质
- 非引用传参:在函数调用时,非引用传参实际上是传递了实参的一个副本给形参。这意味着在函数内部对形参的任何修改都不会影响到原始的实参。非引用传参包括普通形参和指针形参,但指针形参传递的是地址的副本,而不是对象本身的副本。
- 引用传参:引用传参则是将实参的引用(或别名)传递给形参。在函数内部对形参的操作实际上是对实参本身的操作,因此任何修改都会反映到原始的实参上。
所以引用传参不需要复制实参,而是直接操作实参本身,可以节省内存并提高效率。这对于大型对象或数据结构的传递尤为重要。而非引用传参需要复制整个对象或结构,这可能会导致较大的内存开销和较低的执行效率。
2. 左值引用解决了一部分返回值的拷贝问题
string& func()
{static string s = "hello world";return s;
}int main()
{string str1;str1 = func();return 0;
}
引用返回和非引用返回的区别:
- 非引用返回:当函数的返回类型为非引用类型时,函数的返回值用于初始化在调用函数时创建的临时对象。这意味着,如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象。其返回值既可以是局部对象(在函数内部定义的对象),也可以是求解表达式的结果。这种复制过程可能会导致额外的内存开销和性能损失。
- 引用返回:当函数返回类型为引用类型时,没有复制返回值。相反,返回的是对象本身(或更准确地说,是对对象的引用)。因此,返回引用类型通常更高效,因为它避免了不必要的复制操作。但是,需要注意的是,返回引用时必须确保引用的对象在函数返回后仍然有效。
从图中可以看出引用返回比非引用返回少了一次拷贝构造。这是因为返回值s指向的string是全局的,当出了函数作用域依然存在,因此我们可以传引用返回,不用拷贝构造给临时变量,节省了一次拷贝构造。
而在非引用返回函数当中,func函数依然返回hello world
这个字符串,但是s是一个局部变量。出了函数作用域就会被销毁,那么如果str1要想接收到s,那么就会创建一个临时变量拷贝构造给它,然后临时变量再拷贝构造给str。
那么如果我们不想使用引用返回,还想减少一次拷贝,该如何实现呢?答案就是使用右值引用。先来看看有哪些情况下会产生可以被右值引用的左值:
- 当一个左值被move后,可以被右值引用
- C++会把即将离开作用域的非引用类型的返回值当成右值,这种类型的右值也称为
将亡值
在C++11中,把右值分为纯右值和将亡值。
- 纯右值就是内置类型的右值,讲的是用于辨识临时变量和一些不跟对象关联的值。比如非引用返回的函数返回的临时变量值就是一个纯右值。一些运算表达式,比如
1+3
产生的临时变量值,也是纯右值。而不跟对象关联的字母量值,比如:2、‘c’;、true,也是纯右值。 - 将亡值就是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用
T&&
的函数返回值、std::move
的返回值或者转换为T&&
的类型转换函数的返回值。回顾刚才的代码,变量s已经快要离开作用域了,马上就会被销毁,s销毁没有问题,但是字符串hello world
是我们需要的。这种情况可以理解为:一个快要去世的病人,临走前说要把自己的器官捐赠给别人,当然也可以指定捐赠给他人。
同理,一旦左值得到了右值属性,相当于把自己的资源给别人,不希望自己的资源被系统释放,而是被合适的对象继承走。s
即将被销毁,此时s
就是一个右值了,右值的意思就是:这个变量的资源可以被迁移走。这句话非常非常重要!!!
<type_traits>头文件
有的时候,我们可能不知道一个类型是否是引用类型,以及是左值引用还是右值引用(在模板中比较常见)。标准库在<type_traits>
头文件中提供了3个模板类:is_rvalue_reference、is_lvalue_reference、is_reference
,可供我们进行判断。比如:
int main()
{cout << is_rvalue_reference<string&&>::value << endl; //1cout << is_rvalue_reference<string&>::value << endl; //0cout << is_lvalue_reference<string&>::value << endl; //1cout << is_reference<string&>::value << endl; //1
}
我们通过模板类的成员value
就可以打印出string &&
是否是一个右值引用了。配合类型推导符decltype
,我们甚至还可以对变量的 类型进行判断。
移动语义
那么右值是如何把资源迁移走的呢?这就要学习右值引用的移动语义
了:
拷贝构造函数中为指针成员分配新的内存再进行内容拷贝到做法在C++编程中几乎被视为是不可违背的。不过在一些时候,我们确实不需要这样的拷贝构造语义。我们先看下面的代码。
class HasPtrMem
{
public:HasPtrMem() :d(new int(0)){cout << "Construct: " << ++n_cstr << endl;}HasPtrMem(const HasPtrMem& h) :d(new int(*h.d)){cout << "Copy Construct: " << ++n_cptr << endl;}~HasPtrMem(){cout << "Destruct: " << ++n_dstr << endl;}int* d;static int n_cstr;static int n_dstr;static int n_cptr;
};int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;HasPtrMem GetTemp()
{return HasPtrMem();
}int main()
{HasPtrMem a = GetTemp();
}
在代码中,我们声明了一个返回一个HasPtrMem变量的函数。为了记录构造函数、拷贝构造函数,以及析构函数调用的次数,我们使用了一些静态变量。在main函数中,我们简单声明了一个HasPtrMem的变量a,要求它使用GetTemp的返回值进行初始化。运行结果如下(未开启编译器优化):
Construct: 1
Copy construct: 1
Destruct: 1
Copy construct: 2
Destruct: 2
Destruct: 3
首先在GetTemp函数中创建临时对象,调用HasPtrMem构造函数创建临时对象,输出Construct: 1
,接着,GetTemp
函数返回临时对象时进行拷贝构造,调用拷贝构造函数输出Copy construct: 1
,临时对象离开GetTemp
函数作用域,调用析构函数,输出Destruct: 1
。再然后main函数中进行拷贝构造,将 GetTemp()
函数返回的对象拷贝给对象 a
,调用拷贝构造函数,输出 Copy Construct: 2
。GetTemp()
函数返回的对象离开 main
函数中赋值语句的作用域,调用析构函数,输出 Destruct: 2
。最后main函数结束时析构对象a,输出Destruct: 3
。
如果开启了编译器优化,那么GetTemp
函数中创建的临时对象会直接作为对象a进行构造,不会发生拷贝构造。所以开启了编译器优化后,结果是:
接下来我以未开启优化来讲解。在代码中,类HasPtrMem
只有一个int类型指针。而如果HasPtrMem
的指针指向非常大的堆内存数据的话,那么拷贝构造的过程就会非常昂贵。可以想象,这种情况一旦发生,a的初始化表达式的执行速度将相当堪忧。
在main函数部分,按照C++的语义,临时对象将在语句结束后被析构,会释放它所包含的堆内存资源。而a在拷贝构造的时候,又会被分配堆内存。这样的一去一来似乎并没有太大的意义,那么我们是否可以在临时对象构造a的时候不分配内存,即不使用所谓的拷贝构造呢?
在C++11中,答案是肯定的,我们可以看下面的示意图:
上半部分可以看到从临时变量中拷贝构造变量a的做法,即在拷贝时分配新的堆内存,并从临时对象的堆内存中拷贝内容至a.d
。而构造完成后,临时对象将析构,因此其拥有的堆内存资源会被析构函数释放。而下半部分则是一种“新”方法,该方法在构造时使得a.d
指向临时对象的堆内存资源。同时我们保证临时对象对象不释放所指向的堆内存,那么在构造完成后,临时对象被析构,a就从中“偷”到了临时对象所拥有的堆内存资源。
在C++11中,这样的“偷走”临时变量中资源的构造函数,被称为“移动构造函数”。这样的“偷”的行为,则称为“移动语义”。可以理解为“移为己用”。通过下面的代码来看一看如何实现这样的移动语义。
class HasPtrMem
{
public:HasPtrMem() :d(new int(3)){cout << "Construct: " << ++n_cstr << endl;}HasPtrMem(const HasPtrMem& h) :d(new int(*h.d)){cout << "Copy Construct: " << ++n_cptr << endl;}HasPtrMem(HasPtrMem&& h) :d(h.d) // 移动构造函数{h.d = nullptr; // 将临时值的指针成员置为空cout << "Move construct: " << ++n_mvtr << endl;}~HasPtrMem(){delete d;cout << "Destruct: " << ++n_dstr << endl;}int* d;static int n_cstr;static int n_dstr;static int n_cptr;static int n_mvtr;
};int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;HasPtrMem GetTemp()
{HasPtrMem h;cout << "Resource from " << __func__ << ": " << hex << h.d << endl;return h;
}int main()
{HasPtrMem a = GetTemp();cout << "Resource from " << __func__ << ": " << hex << a.d << endl;
}
对比刚才的代码,这个代码多了一个构造函数HasPtrMem(HasPtrMem &&)
,这个就是移动构造函数。与拷贝构造函数不同的是,移动构造函数接受一个所谓的“右值引用”的参数。移动构造函数使用了参数h的成员d初始化了本对象的成员d(而不是像构造函数一样需要分配内存,然后将内容依次拷贝到新分配的内存中),而h的成员d随后被置为nullptr
。这样就完成了移动构造的全过程。
代码运行结果如下(未开启优化)
Construct: 1
Resource from GetTemp: 0x603010
Move construct: 1
Destruct: 2
Move construct: 2
Destruct: 2
Resource from main: 0x603010
Destruct: 3
可以看到,这里没有调用拷贝构造函数,而是调用了两次移动构造函数,移动构造函数的结果是,GetTemp
中的h的指针成员h.d
和main函数中的a的指针成员a.d的值是相同的,即h.d
和a.d
都指向了相同的堆地址内存。该堆内存在函数返回的过程中,成功的逃避了被析构的命运,取而代之地,成为了赋值表达式中的变量a的资源。如果堆内存不是一个int长度的数据,而是以mbyte
为单位的堆空间,那么这样的移动带来的性能提升是非常惊人的。
std::move()
在C++11中,标准库<utility>
中提供了一个有用的函数std::move
,这个函数的名字具有迷惑性,因为实际上move
并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而我们可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move
基本等同于一个类型转换:static_cast<T&&>(lvalue);
。
值得一提的是,被转化的左值,其生命期并没有随着左右值的转化而改变。来看下面的示例:
class Moveable
{
public:Moveable():i(new int(3)){}~Moveable() { delete i; }Moveable(const Moveable & m):i(new int(*m.i)){}Moveable(Moveable&& m) :i(m.i){m.i = nullptr;}int* i;
};int main()
{Moveable a;Moveable c(move(a)); // 会调用移动构造函数cout << *a.i << endl;
}
在代码中,我们为类型Moveable
定义了移动构造函数。这个函数定义本身没有什么问题,但是调用的时候,使用了Moveable c(move(a));
这样的语句。这里的a本来是一个左值变量,通过move后变成右值。这样一来,a.i
就被c的移动构造函数设置为指针空值。由于a的生命周期实际要到main
函数结束才结束,那么随后的表达式*a.i
进行计算的时候,就会发生严重的运行时错误。
来看一看正确的代码。
class HugeMem
{
public:HugeMem(int size) :sz(size > 0 ? size : 1) {c = new int[sz];}~HugeMem() { delete[]c; }HugeMem(HugeMem&& hm) :sz(hm.sz), c(hm.c) {hm.c = nullptr;}int* c;int sz;
};class Moveable
{
public:Moveable() :i(new int(3), h(1024) {}~Moveable() { delete i; }Moveable(Moveable && m) :i(m.i), h(move(m.h)) // 强制转化为右值,以调用移动构造函数{m.i = nullptr;}int* i;HugeMem h;
};Moveable GetTemp()
{Moveable tmp = Moveable();cout << hex << "Huge Mem from " << __func__ << " @" << tmp.h.c << endl;// Huge Mem from GetTemp @0x603030return tmp;
}int main()
{Moveable a(GetTemp());cout << hex << "Huge Mem from " << __func__ << " @" << a.h.c << endl;// Huge Mem from GetTemp @0x603030return 0;
}
在代码中,我们定义了两个类型:HugeMem
和Moveable
,其中Moveable
包含了一个HugeMem
的对象。在Moveable
的移动构造函数中,我们就看到了std::move
函数的使用。该函数将m.h
强制转化为右值,以迫使Moveable
中的h能够实现移动构造。这里也可以使用std::move
,是因为m.h是m的成员,既然m将存在表达式结束后被析构,其成员也自然会被析构,因此不存在上一个代码中生存期不对的问题。
那么如果不使用std::move(m.h)
这样的表达式,而是直接使用m.h
这个表达式会怎么样?这里的m.h
引用了一个确定的对象,而且m.h
也有名字,可以使用&m.h
取到地址,因此是一个不折不扣的左值。不过这个左值确实会很快“灰飞烟灭”,因为拷贝构造函数在Moveable
对象a的构造完成后也就结束了。那么这里使用std::move
强制转为右值就不会有问题了。而且,如果我们不这么做,由于m.h
是个左值,就会导致调用HugeMem
的拷贝构造函数来构造Moveable
的成员h。如果是这样,移动语义就没有能够成功地向类的成员传递。换言之,还是会由于拷贝而导致一定的性能上的损失。
事实上,为了保证移动语义的传递,在编写移动构造函数的时候,应该总是记得使用move
转换拥有形如堆内存、文件句柄等资源的成员为右值,这样一来,如果成员支持移动构造的话,就可以实现其移动语义。而即使成员没有移动构造函数,那么接受常量左值的构造函数版本也会轻松地实现拷贝构造,因此也不会引起大的问题。
完美转发
完美转发,是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。比如:
template <typename T>
void IamForwording(T t) {IrunCodeActually(t); }
这个例子中,IamForwording
是一个转发函数模板。而函数IrunCodeActually
则是真正执行代码的目标函数。对于目标函数IrunCodeActually
而言,它总是希望转发函数将参数按照传入Iamforwarding
时的类型传递(即传入的是左值对象,IrunCodeActually
就能获得左值对象,传入右值是也是一样),而不产生额外的开销,就好像转发者不存在一样。
这似乎是一件很容易的事情,但其实并不简单。在上面的例子中,在IamForwarding
的参数中使用了最基本类型进行转发,该方法会导致参数在传给IrunCodeActually
之前就产生了一次额外的临时对象拷贝。因此这样的转发只能说是正确的转发,但谈不上完美。
所以通常程序需要对是一个引用类型,引用类型不会产生额外的开销。其次,则需要考虑转发函数对类型的接收能力。因为目标函数可能需要既能够接受左值引用,又接受右值引用。那么如果转发函数只能接受其中的一部分,就无法做到完美转发。那么如果使用“万能”的常量左值类型呢?以常量左值为参数的转发函数会一些尴尬,比如:
void IrunCodeActually(int t) {}
template <typename T>
void IamForwording(const T& t) {IrunCodeActually(t); }
由于目标函数的参数类型是非常量左值类型,因此无法接受常量左值引用作为参数,这样一来,虽然转发函数的接受能力很高,但在目标函数的接受上却出了问题。那么我们可能就需要通过一些常量和非常量的重载来解决目标函数的接受问题。这在函数参数比较多的情况下,就会造成代码冗余,而且根据上面的表格中,如果我们的目标函数的参数是个右值引用的话,同样无法接受任何左值类型作为参数,间接的,也就导致无法使用移动语义。
那么C++11如何解决完美转发的问题的呢?实际上,C++11是通过“引用折叠”的新语言规则,并结合新的模板推导规则来完成完美转发。
在C++11以前,例如下面的语句:
typedef const int T;
typedef T& TR;
TR& v = 1; // 该声明在C++98中会导致编译错误
其中TR& v = 1
这样的表达式会被编译器认为是不合法的表达式,而在C++11中,一旦出现了这样的表达式,就会发生引用折叠,即将复杂的未知表达式折叠为已知的简单表达式,如下表。
TR的类型定义 | 声明v的类型 | v的实际类型 |
---|---|---|
T& | TR | A& |
T& | TR& | A& |
T& | TR&& | A& |
T&& | TR | A&& |
T&& | TR& | A& |
T&& | TR&& | A&& |
规则并不难记,因为一旦定义中出现了左值引用,引用折叠总是优先将其折叠为左值引用。而模板对类型的推导规则比较简单,当转发函数的实参是类型X的一个左值引用,那么目标参数被推导为X&
类型,而转发函数的实参是类型X的一个右值引用的话,那么模板的参数被推导为X&&
类型。结合以上的折叠规则,就能确定出参数的实际类型。进一步,我们可以把转发函数写成如下形式:
template <typename T>
void IamForwording(T&& t)
{IrunCodeActually(static_cast<T && > (t));
}
我们不仅在参数部分使用了T &&
这样的标识,在目标函数传参的强制类型转换中也使用了这样的形式。比如我们调用转发函数时传入了一个X类型的左值引用,可以想象,转发函数将被实例化为如下形式:
void IamForwording(X& && t)
{IrunCodeActually(static_cast<X& && > (t));
}
引用折叠规则就是:
void IamForwording(X& t)
{IrunCodeActually(static_cast<X&> (t));
}
对于一个右值而言,当它使用右值引用表达式引用的时候,该右值引用却是个左值,那么我们想在函数调用中继续传递右值,就需要使用move
来进行左右值的转换。而move
通常就是一个static_cast
。不过在C++11中,用于完美转发的函数却不再叫做move
,而是另外一个名字:forward
。所以我们可以把转发函数写成这样:
template <typename T>
void IamForwording(T && t)
{IrunCodeActually(forward(t));
}
move
和forward
在实际实现上差别并不大。来看一个完美转发的代码:
void RunCode(int&& m) { cout << "rvalue ref" << endl; }
void RunCode(int& m) { cout << "lvalue ref" << endl; }
void RunCode(const int&& m) { cout << "const rvalue ref" << endl; }
void RunCode(const int& m) { cout << "const lvalue ref" << endl; }template<typename T>
void PerfectForward(T&& t) { RunCode(forward<T>(t)); }int main()
{int a;int b;const int c = 1;const int d = 0;PerfectForward(a); // lvalue refPerfectForward(move(b)); // rvalue refPerfectForward(c); // const lvalue refPerfectForward(move(d)); // const rvalue ref
}
可以看到,所有的转发都被正确地送到了目的地。
完美转发的一个作用就是包装函数,这是一个很方便的功能,对上面的代码稍作修改,就可以用很少的代码记录单参数函数的参数传递状况。
template<typename T, typename U>
void PerfectForward(T&& t, U& Func)
{cout << t << "\tforwarded..." << endl;Func(forward<T>(t));
}void RunCode(double && m) {}
void RunHome(double && h) {}
void RunComp(double && c) {}int main()
{PerfectForward(1.5, RunComp); // 1.5 forwarded...PerfectForward(8, RunCode); // 8 forwarded...PerfectForward(1.5, RunHome); // 1.5 forwarded...
}
相关文章:
右值引用的学习
传统的C语法中就有引用的语法,而C11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。 左值引用和右值引用 在讲之前,我们先来看一下什么是左值和右值…...
碎片笔记|AI生成图像溯源方法源码复现经验(持续更新中……)
前言:本篇博客分享一些溯源方法的复现经验,希望能帮助到大家🎉。 目录 1. Close-set AttributionRepmixDe-FakeDNA-Net 2. Open-set AttributionPOSE 3. Single-Model AttributionOCC-CLIPLatentTracer 1. Close-set Attribution Repmix 论…...
elementplus el-tree 二次封装支持配置删除后展示展开或折叠编辑复选框懒加载功能
本文介绍了基于 ElementPlus 的 el-tree 组件进行二次封装的 TreeView 组件,使用 Vue3 和 JavaScript 实现。TreeView 组件通过 props 接收树形数据、配置项等,支持懒加载、节点展开/收起、节点点击、删除、编辑等操作。组件内部通过 ref 管理树实例&…...
【C/C++】深度探索c++对象模型_笔记
1. 对象内存布局 (1) 普通类(无虚函数) 成员变量排列:按声明顺序存储,但编译器会根据内存对齐规则插入填充字节(padding)。class Simple {char a; // 1字节(偏移0)int b; …...
Spring MVC数据绑定和响应 你了解多少?
数据绑定的概念 在程序运行时,Spring MVC接收到客户端的请求后,会根据客户端请求的参数和请求头等数据信息,将参数以特定的方式转换并绑定到处理器的形参中。Spring MVC中将请求消息数据与处理器的形参建立连接的过程就是Spring MVC的数据绑…...
外贸礼品禁忌
一、亚洲 1.印度 牛是神圣动物,别送牛皮制品。另外,左手不洁,送礼得用右手或双手。 2.日本 “梳” 和 “苦” 谐音,不送梳子。日本男性不咋佩戴首饰,除结婚戒指。礼物得装盒、纸包、绳饰,白色包装得有…...
【MySQL 基础篇】深入解析MySQL逻辑架构与查询执行流程
1 MySQL逻辑架构概述 MySQL 的逻辑架构主要分为 Server 层和存储引擎层两部分。 Server 层:包含连接器、查询缓存、分析器、优化器、执行器等组件。同时,所有的内置函数(如日期、时间、数学和加密函数等)也在这一层实现。此外&a…...
基于C#实现中央定位服务器的 P2P 网络聊天系统
基于中央定位服务器的 P2P 网络聊天系统 1 需求分析与实现功能 本次作业旨在实现一个基于中央定位服务器的 P2P 网络聊天系统,也即通过中央定位服务器实现登入,登出与好友的 IP 查询等操作,在好友间的通信使用 P2P 来完成,具体见…...
【C++】map和set的模拟实现
1.底层红黑树节点的定义 enum Colur {RED,BLACK }; template<class T> struct RBTreeNode {RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;T _data;Colur _col;RBTreeNode(const T& data):_left(nullptr), _right(nullptr…...
数据结构·字典树
字典树trie 顾名思义,在一个字符串的集合里查询某个字符串是否存在树形结构。 树存储方式上用的是结构体数组,类似满二叉树的形式。 模板 定义结构体和trie 结构体必须的内容:当前结点的字符,孩子数组可选:end用于查…...
centos服务器,疑似感染phishing家族钓鱼软件的检查
如果怀疑 CentOS 服务器感染了 Phishing 家族钓鱼软件,需要立即进行全面检查并采取相应措施。以下是详细的检查和处理步骤: 1. 立即隔离服务器 如果可能,将服务器从网络中隔离,以防止进一步传播或数据泄露。如果无法完全隔离&…...
(C语言)超市管理系统(测试2版)(指针)(数据结构)(清屏操作)
目录 前言: 源代码: product.h product.c fileio.h fileio.c main.c 代码解析: 一、程序结构概述 二、product.c 函数详解 1. 初始化商品列表 Init_products 2. 添加商品 add_product 3. 显示商品 display_products 4. 修改商品 mo…...
可变形卷积简介(Deformable Convolution)
1. 核心原理 可变形卷积通过动态调整卷积核的采样位置,增强模型对几何形变(如旋转、缩放)的适应能力。其核心改进包括: 偏移量(Offset):为卷积核的每个采样点学习 x / y x/y x/y方向的偏移量 …...
02_Servlet
目录 一、简介二、Servlet入门案例2.1、编写Servlet2.2、配置Servlet2.3、访问Servlet2.4、常见错误 三、Servlet详解3.1、实现Servlet的三种方式3.1.1、实现Servlet接口3.1.2、继承GenericServlet类3.1.3、继承HttpServlet类 3.2、配置Servlet的两种方式3.2.1、web.xml方式3.2…...
stm32之FLASH
目录 1.简介2.闪存模块组织3.基本结构3.1 FPEC3.2 程序存储器3.2.1 标准编程3.2.2 页擦除3.2.3 全擦除 3.3 选项字节3.3.1 编程3.3.2 擦除 4.器件电子签名5.实验-读取内部FLASH 1.简介 STM32F1系列的FLASH内存是一个非常重要的存储区域,它主要由三个部分组成&#…...
第3.4节 调用链路分析服务开发
3.4.1 什么是Code Call Graph(CCG) Code Call Graph(CCG)即业务代码中的调用关系图,是通过静态分析手段分析并构建出的一种描述代码间关系的图。根据精度不同,一般分为类级别、方法级别、控制流级别&#x…...
超详细Docker教程
前言:大家在在Linux上部署mysql及其他软件时,大家想一想自己最大的感受是什么? 我相信,除了个别天赋异禀的人以外,大多数人都会有相同的感受,那就是麻烦。核心体现在三点: 命令太多了ÿ…...
探索AI新领域:生成式人工智能认证(GAI认证)助力职场发展
在数字化时代的大潮中,人工智能(AI)技术以其强大的影响力和广泛的应用前景,正逐步重塑我们的生活与工作方式。随着生成式AI技术的崛起,掌握这一前沿技能已成为职场竞争中的关键优势。那么,如何通过系统的学…...
sql sql复习
虽然之前学习过sql,但由于重在赶学习进度,没有学扎实,导致自己刷题的时候有的地方还是模模糊糊,现在主要是复习,补一补知识点。 今日靶场: NSSCTF 云曦历年考核题 在做题之前先回顾一下sql注入的原理&…...
初探 Skynet:轻量级分布式游戏服务器框架实战
在游戏服务器开发领域,高效、稳定且易于扩展的框架一直是开发者追求的目标。Skynet 作为一款轻量级、高性能的分布式游戏服务器框架,凭借其独特的设计理念和强大的功能,赢得了众多开发者的青睐 一.Skynet底层架构支持 1.Actor erlang 从语言…...
libarchive.so.19丢失
文章目录 绝对路径可以启动,相对路径不可以以绝对路径启动conda环境,运行python3.8(成功) 报错 Error while loading conda entry point: conda-libmamba-solver (libarchive.so.19: cannot open shared object file: No such file or directory) sudo a…...
vue-ganttastic甘特图label标签横向滚动固定方法
这个甘特图之前插件里,没有找到能固定label标签在屏幕上的办法,用css各种办法都没有实现,所以我我直接手写定位,用js监听滚动条滚动的距离,然后同步移动甘特图label标签,造成一种定位的错觉,以下…...
自动化 NuGet 包打包与上传:完整批处理脚本详解(含 SVN 支持)
在大型项目中,我们常常需要定期打包多个 .csproj 项目为 NuGet 包,并上传到私有 NuGet 服务。这篇文章分享一份实战脚本,支持以下自动化流程: 自动读取、更新 .csproj 文件中的 Version、PackageOutputPath 等节点; 自…...
Go语言空白导入的作用与用途
在 Go 语言中,导入包时前面加下划线 _ 是一种特殊的导入方式,称为 “空白导入” 或 “匿名导入”。 作用: 执行包的初始化(init 函数)但不直接使用包中的标识符 import _ "go.uber.org/automaxprocs" 表示你…...
实验六:按键模拟控制实现
FPGA序列检测器实验(远程实验系统) 文章目录 FPGA序列检测器实验(远程实验系统)一、数字电路基础知识1. 时钟与同步2. 按键消抖原理代码讲解:分频与消抖3. 有限状态机(FSM)设计代码讲解:状态机编码与转移4. 边沿检测与信号同步5. 模块化设计二、实验数字电路整体思想三…...
【愚公系列】《Manus极简入门》038-数字孪生设计师:“虚实映射师”
🌟【技术大咖愚公搬代码:全栈专家的成长之路,你关注的宝藏博主在这里!】🌟 📣开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主! …...
Linux重定向与缓冲区
目录 文件描述符的分配规则 重定向 使用 dup2 系统调用 FILE 文件描述符的分配规则 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>int main() {int fd open("myfile", O_RDONLY);if(fd < 0){per…...
经典还原反应解析:Wolff-Kishner机制与黄鸣龙改进法
在有机化学发展史上记载的万余种经典反应中,当论及以科学家命名的标志性转化反应时,Wolff-Kishner-黄鸣龙还原反应必然占据重要地位。在大学《有机化学》课程中,学习还原反应时肯定会提到Wolff-Kishner-黄鸣龙还原反应,这是第一个…...
DataX从Mysql导数据到Hive分区表案例
0、下载DataX并解压到对应目录 DataX安装包,开箱即用,无需配置。 https://datax-opensource.oss-cn-hangzhou.aliyuncs.com/202308/datax.tar.gz 相关参考文档 https://github.com/alibaba/DataX/blob/master/hdfswriter/doc/hdfswriter.md 1、Hive分区…...
npm 报错 gyp verb `which` failed Error: not found: python2 解决方案
一、背景 npm 安装依赖报如下错: gyp verb check python checking for Python executable "python2" in the PATH gyp verb which failed Error: not found: python2 一眼看过去都觉得是Python环境问题,其实并不是你python环境问题…...
安装npm:npm未随Node.js一起安装
文章目录 上传至linux服务器/usr/local/目录下 如果npm没有随Node.js一起安装,你可以尝试单独下载并安装npm。但通常情况下,这是不必要的,因为npm是Node.js的一部分。如果确实需要单独安装npm,你可以参考npm的官方安装指南。 npm…...
C++23 ranges::to:范围转换函数 (P1206R7)
文章目录 引言C23 Ranges 概述ranges::to 的定义与功能定义功能 使用场景范围转换为容器简化字符串解析映射转换为向量 ranges::to 的优势代码简洁性提高开发效率与C23的stl容器的范围版本构造函数配合良好 模板参数约束的思考总结 引言 在C的发展历程中,每一个新版…...
openfeign 拦截器实现微服务上下文打通
目录 openfeign 拦截器实现微服务上下文打通需求分析:代码实现:subject 服务:controllerFeign 拦截器:将 Feign 拦截器注册为一个Bean: auth 鉴权服务:全局配置类:登录拦截器:上下文…...
【MySQL】变更缓冲区:作用、主要配置以及如何查看
📢博客主页:https://blog.csdn.net/2301_779549673 📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson 📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! &…...
TCP/IP-——C++编程详解
1. TCP/IP 编程基本概念 TCP(传输控制协议):面向连接、可靠的传输层协议,保证数据顺序和完整性。IP(网际协议):负责将数据包路由到目标地址。Socket(套接字):…...
微服务如何实现服务的高可用
背景:微服务分层需要考虑高可用和高并发的问题 微服务如何实现服务的高可用 先说结论:微服务实现高可用主要通过集群冗余故障自动转移来实现的具体可以从底下几种方案来实现。 “端”到“反向代理”的高可用“反向代理”到“站点应用”的高可用“站点…...
微服务调试问题总结
本地环境调试。 启动本地微服务,使用公共nacos配置。利用如apifox进行本地代码调试解决调试问题。除必要的业务微服务依赖包需要下载到本地。使用mvn clean install -DskipTests进行安装启动前选择好profile环境进行启动,启动前记得mvn clean清理项目。…...
egpo进行train_egpo训练时,keyvalueError:“replay_sequence_length“
def execution_plan(workers: WorkerSet, config: TrainerConfigDict) -> LocalIterator[dict]: if config.get(“prioritized_replay”): prio_args { “prioritized_replay_alpha”: config[“prioritized_replay_alpha”], “prioritized_replay_beta”: config[“prior…...
Hadoop的组成
(一)Hadoop的组成 对普通用户来说, Hadoop就是一个东西,一个整体,它能给我们提供无限的磁盘用来保存文件,可以使用提供强大的计算能力。 在Hadoop3.X中,hadoop一共有三个组成部分&#…...
Android锁
引言 🔒 在 Android 应用的开发过程中,随着业务需求的复杂度不断提升,多线程并发场景层出不穷。为了保证数据一致性与线程安全,锁(Lock)成为了不可或缺的工具。本篇博客将深入剖析 Android 中常用的锁机制…...
XD08M3232接近感应单片机的接近感应模块的工作原理
XD08M3232接近感应单片机的接近感应模块基于电容式感应原理,通过硬件电路与软件配置实现对物体接近的检测。以下是其工作原理的详细解析: 一、硬件架构与核心组件 1. 核心电路组成 接近感应模块由三大关键部分构成: 两个轨到轨运算放大器&…...
编程日志5.6
串的习题 1.2236. 判断根结点是否等于子结点之和 - 力扣(LeetCode) /** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * Tr…...
量子计算实用化突破:从云端平台到国际竞合,开启算力革命新纪元
在硅谷某生物医药实验室,研究员艾米丽正盯着量子计算模拟界面露出微笑 —— 搭载中电信 "天衍" 量子计算云平台的 880 比特超导量子处理器,用 17 分钟完成了传统超算需 3 个月才能跑完的新型抗生素分子键合模拟。这个场景标志着量子计算正从 &…...
Made with Unity | 拓展“双点”宇宙版图
双点工作室(Two Point Studios)团队成立于2016年,其创立初衷是打造一个完美的游戏IP:构建一个既能持续吸引玩家,又具备足够扩展空间,同时经得起时间考验的虚拟世界。2018年,团队以《双点医院&am…...
3D 数据可视化系统是什么?具体应用在哪方面?
目录 一、3D 数据可视化系统的定义与内涵 (一)基本概念 (二)核心要素 二、3D 数据可视化系统的优势 三、3D 数据可视化系统的应用领域 (一)城市规划与管理 (二)工业制造 &am…...
2025-5-14渗透测试:利用Printer Bug ,NTLMv2 Hash Relay(中继攻击),CVE-2019-1040漏洞复现
python3 printerbug.py test.com/test192.168.186.131 192.168.186.134 sudo python2 MultiRelay.py -t 192.168.186.132 -u ALLPrinter Bug 原理 PrinterBug(CVE-2018-0886)是Windows打印系统服务(Spooler)的一个设计缺陷&…...
OracleLinux7.9-ssh问题
有套rac环境,db1主机无法ssh db1和db1-priv,可以ssh登录 db2和db2-priv [rootdb1 ~]# ssh db1 ^C [rootdb1 ~]# ssh db2 Last login: Wed May 14 18:25:19 2025 from db2 [rootdb2 ~]# ssh db2 Last login: Wed May 14 18:25:35 2025 from db1 [rootdb2…...
《AI大模型应知应会100篇》第64篇:构建你的第一个大模型 Chatbot
第64篇:构建你的第一个大模型 Chatbot 手把手教你从零开始搭建一个基于大模型的聊天机器人 摘要 你是否想过,自己也能构建一个像 ChatGPT 一样能对话、能思考的聊天机器人(Chatbot)?别担心,这并不需要你是…...
鸿蒙OSUniApp 实现精美的用户登录和注册页面#三方框架 #Uniapp
UniApp 实现精美的用户登录和注册页面 前言 在开发鸿蒙APP时,登录注册页面作为用户与应用交互的第一道门槛,其体验与质量往往决定了用户的第一印象。去年我接手了一个电商小程序项目,产品经理特别强调要做一个既美观又实用的登录注册页面。…...
c#中equal方法与gethashcode方法之间有何关联?
文章目录 前言一、对hash运算的深入思考二、equal与gethashcode的关联三、 equal与gethashcode不同步的后果四、 规范的重写gethashcode 前言 大家有没有遇到过,当你重写了c#对象的equal方法之后,编译器会提示你对相应的gethashcode进行重写,…...