c++11--左值,右值,移动语义,引用折叠,模板类型推断,完美转发

发布时间:2026/7/29 17:35:33

c++11--左值,右值,移动语义,引用折叠,模板类型推断,完美转发 1.移动语义移动构造和移动赋值均属于移动语义范畴。移动语义的实现依赖于右值概念右值引用。1.1.一个移动构造的实例#include iostream using namespace std; 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(){ if(d) { 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;// 1.局部对象构造 cout Resource from __func__ : hex h.d endl; return h; }// 3.局部对象析构 int main() { // 2.匿名对象拷贝构造--实参为即将析构的局部变量时匹配到右值版本 // 4.对象a拷贝构造实参为匿名临时对象时匹配到右值版本 HasPtrMem a GetTemp(); // 3.匿名对象析构 cout Resource from __func__ : hex a.d endl; // 4.对象a析构 }对其执行g -stdc11 44.cpp -fno-elide-constructors -o 44再执行./.44。上述以函数内临时变量为实参拷贝构造匿名临时变量以匿名临时变量为实参拷贝构造变量a时均触发了移动拷贝构造。移动拷贝构造的形参是一个右值引用类型。我们需要先理解(1). 左值右值。(2). 左值引用常量左值引用右值引用作为形参时可接收的实参。及各类实参和形参匹配的优先级。才能较好的把握移动拷贝移动赋值的使用。从理解上来说针对需要管理资源的类型我们将拷贝和赋值区分为浅拷贝浅赋值和深拷贝深赋值。浅指的是我们通过窃取目标对象资源来完成资源转移。深指的是我们自己产生资源然后使其和目标对象内容一致。这种场景下类型通过提供移动拷贝移动赋值来实现浅拷贝浅赋值。通过提供拷贝构造赋值来实现深拷贝深赋值。1.2.左值右值c程序中所有的值必属于左值将亡值纯右值三者之一。将亡值纯右值均属于右值范畴。(1). 纯右值和C中右值概念一致。a. 非引用返回的函数返回的匿名临时变量值。b. 一些运算表达式诸如1 3产生的匿名临时变量值。c. 不跟对象关联的字面量值比如2、c、true。d. 类型转换函数返回的匿名临时变量值。e.lambda表达式返回的匿名临时变量值。(2). 将亡值C特有。a. 返回右值引用T的函数返回值–匿名右值引用类型。b.std::move的返回值–匿名右值引用类型。c. 转换为T的类型转换函数的返回值–匿名右值引用类型。(3). 左值排除(1)(2)后的可标识函数对象值都属于左值。1.3.非常量左值引用常量左值引用非常量右值引用常量右值引用常量和非常量左值和右值是可以同时用于修饰对象的两类性质。右值存在的价值一个是用于实现移动语义一个是用于实现完美转发。一般常量右值引用没有存在的意义因为(1). 右值用于移动语义时我们窃取其资源后需要对其修改以便阻止资源多次释放野指针等问题。(2). 如果需要引用右值且让右值不可通过引用更改直接使用常量左值来引用即可。使用右值作为实参来初始化右值引用常量左值引用后右值的生命期会被延长。通过右值引用可以对引用对象进行修改通过常量左值引用无法对引用对象进行修改。#include iostream class A { public: A() { printf(A()_%x\n, this); } A(const A a) { printf(A(const A_%x)\n, this); } ~A() { printf(~A()_%x\n, this); } public: int i 0; }; A fun() { A a; return a; } int main() { // 这里的变量除了a1还有一个隐藏起来的匿名临时变量。 // 汇编执行时 // 是匿名临时变量先接受了函数的返回值 // 再通过匿名临时变量完成了a1的初始化 // 这里函数内部临时变量a,变量a1均是左值 // 匿名临时变量是右值 A a1 fun(); printf(tag1\n); A a2 fun(); printf(tag2\n); a2.i 1; const A a3 fun(); printf(tag3\n); return 0; }上述实例反映了两点(1). 通过采用右值引用或常量左值引用接受函数fun的非引用类型返回值。使得返回值对应的匿名临时右值对象的生命期延长了。(2). 通过右值引用可以修改所引用变量通过常量左值引用则不可。以下表格整理了引用类型及每种类型可以接受的初始化实参类型。引用类型可以引用的值类型注记非常量左值引用非常量左值常量左值引用非常量左值常量左值非常量右值常量右值全能类型用于深拷贝非常量右值引用非常量右值移动语义完美转发常量右值引用非常量右值常量右值1.4.std::move#include iostream class A { public: A():m_c(a){ printf(A()_%x\n, this); } A(A a):m_c(a.m_c){ printf(A()_%x\n, this); fun(a); } A(const A a):m_c(a.m_c){ printf(A(const)_%x\n, this); fun(a); } A(A a):m_c(a.m_c){ printf(A()_%x\n, this); fun(a); } A(const A a):m_c(a.m_c) { printf(const A()_%x\n, this); fun(a); } A operator(A a) { printf((A)_%x\n, this); m_c a.m_c; fun(a); return *this; } A operator(const A a) { printf((const A)_%x\n, this); m_c a.m_c; fun(a); return *this; } A operator(A a) { printf((A)_%x\n, this); m_c a.m_c; fun(a); return *this; } A operator(const A a) { printf((const A)_%x\n, this); m_c a.m_c; fun(a); return *this; } void fun(A a) { printf(fun(A)_%x\n, this); } void fun(const A a) { printf(fun(const A)_%x\n, this); } void fun(A a) { printf(fun(A)_%x\n, this); } void fun(const A a) { printf(fun(constA)_%x\n, this); } ~A() { printf(~A()_%x\n, this); } private: char m_c; }; void funn(A a) { } int main() { A a; const A a2; printf(tag\n); // std::move针对非常量左值返回非常量右值引用。 // 非常量右值引用虽然是一个非常量右值的别名但由于有了名字。所以右值引用被当成一个左值。 // 故通过非常量右值引用调用fun时匹配到非常量左值引用版本。 A a3 std::move(a); printf(tag1\n); // std::move针对常量左值返回常量右值引用。 // 常量右值引用虽然是一个常量右值的别名但由于有了名字。所以右值引用被当成一个左值。 // 故通过常量右值引用调用fun时匹配到常量左值引用版本。 A a4 std::move(a2); printf(tag2\n); // std::move针对非常量右值返回非常量右值引用。 // 非常量右值引用虽然是一个非常量右值的别名但由于有了名字。所以右值引用被当成一个左值。 // 故通过非常量右值引用调用fun时匹配到非常量左值引用版本。 A a5 std::move(A()); printf(tag3\n); A aa1 a; // std::move针对非常量左值引用返回非常量右值引用。 A aaa1 std::move(aa1); printf(tag4\n); const A aa2 a2; // std::move针对常量左值引用返回常量右值引用。 A aaa2 std::move(aa2); printf(tag5\n); A aa3 A(); // std::move针对非常量右值引用返回非常量右值引用。 A aaa3 std::move(aa3); printf(tag6\n); const A aa4 A(); // std::move针对常量右值引用返回常量右值引用。 A aaa4 std::move(aa4); printf(tag7\n); A b; funn(b); printf(tag5\n); return 0; }针对std::move简单的总结是(1). 针对左值实参左值引用实参会返回此实参的右值引用类型。(2). 针对右值实参右值引用实参会返回此实参的右值引用类型。(3). 保持实参的常量属性不变。注意点(1). 右值引用虽然是一个右值的引用但由于引用本身是有名字的所以右值引用是一个左值类型。(2).std::move返回实参的右值引用但这里相当于一个得到了匿名右值引用。所以将此返回值传参调用函数时右值引用会被当成右值类型。而非(1)中的左值类型。关于std::move保持实参的常量属性不变的辅助实例#include iostream void fun(int* a) { printf(int* \n); } void fun(const int* a) { printf(const int* \n); } void fun(int* const a) { printf(int* const \n); } void fun(const int* const a) { printf(const int* const \n); } int main() { int a1 10; const int a11 10; int *a a1; const int* a2 a11; int * const a3 a1; const int* const a4 a11; fun(std::move(a)); fun(std::move(a2)); fun(std::move(a3)); fun(std::move(a4)); return 0; }#includeiostream#includevectorvoidfun(constinta){printf(const int\n);}voidfun(inta){printf(int\n);}intmain(){inti0;fun(std::move(i));intjstd::move(i);fun(j);return0;}1.5.编译器的返回值优化上述各个观察函数返回值的实例我们编译时加了-fno-elide-constructors选项来关闭编译器针对返回值的优化。当开启此优化时#include iostream class A { public: A(){ printf(A()_%x\n, this); } A(Aa){ printf(A()_%x\n, this); } A(const A){ printf(A(const A)_%x\n, this); } A(A){ printf(A()_%x\n, this); } A(const A){ printf(A(const)_%x\n, this); } ~A(){ printf(~A()_%x\n, this); } }; A fun() { A a; return a; } int main() { A b fun(); return 0; }b直接霸占了fun内部的变量a。此优化并不是对任何情况均有效。还有一些情形即使存在此优化也不能达到最好效果。而移动语义总是可以在显式控制下采用最有效的方法实现目标。2.完美转发完美转发指的是在函数模板中完全依照模板参数的类型将参数传递给函数模板中调用的另外一个函数。2.1.引用折叠为了支持完美转发c既需要引入右值右值引用。也需要引入引用折叠。引用折叠进一步分为左值引用折叠右值引用折叠。// TR的类型定义为T声明v的类型为TR - v的实际类型为T又因为T为const int所以v的实际类型为const int所以TR v 1;对应const int v 1; #include iostream typedef const int T; typedef T TR; int main() { TR v 1; }TR的类型定义声明v的类型v的实际类型TTRTTTRTTTRTTTRTTTRTTTRT将上述引用折叠结合模板类型推断可以实现完美转发。2.2.模板类型推断当模板的形参中对模板类型采用T的形式时推断T的类型时当转发函数的实参是类型X的一个左值或左值引用则模板参数T被推导为X类型。当转发函数的实参是类型X的一个右值或右值引用则模板参数T被推导为X类型。此时需结合引用折叠规则来最终确定参数的实际类型。一个实例templatetypename T void IamForwording(T t){ IrunCodeActually(static_castT(t)); }对于上述形式的转发函数如果我们调用转发函数时传入了一个X类型的左值或左值引用。则(1). 模板参数T被推断为X类型。(2). 引用折叠完成后的实际的转发函数是void IamForwording(X t){ IrunCodeActually(X(t)); }这样通过左值或左值引用调用转发函数最终调用实际函数的左值引用版本。如果我们调用转发函数时传入了一个X类型的右值或右值引用。则(1).模板参数T被推断为X类型。(2).引用折叠完成后的实际的转发函数是void IamForwording(X t){ IrunCodeActually(Xt); }这样通过右值或右值引用调用转发函数最终调用实际函数的右值引用版本。上述Xt并不多余因为若直接采用t的形式调用实际函数由于右值引用类型自身是左值所以将匹配到左值引用版本。2.3.完美转发实例#include iostream using namespace std; void RunCode(int m){ cout int endl; } void RunCode(int m){ cout int endl; } void RunCode(const int m) { cout const int endl; } void RunCode(const int m) { cout const int endl; } templatetypename T void PerfectForward(T t){ RunCode(forwardT(t)); } int main(){ int a; int b; const int c 1; const int d 0; PerfectForward(a); PerfectForward(move(b)); PerfectForward(c); PerfectForward(move(d)); }将上述转发函数改为如下形式一样可以达到效果templatetypename T void PerfectForward(T t){ RunCode(static_castT(t)); }若上述模板形参变成如下templatetypename T void PerfectForward(T t){ RunCode(static_castT(t)); } int main(){ int a; int b; const int c 1; const int d 0; PerfectForward(a); PerfectForward(move(b)); PerfectForward(c); PerfectForward(move(d)); }则执行PerfectForward(move(b));会报错因为此时无法完成将一个右值引用匹配到【T推断为intTt的实际类型为int】int的转换。而PerfectForward(move(d));可以执行此时是一个常量右值引用匹配到【T推断为const intTt的实际类型为const int】const int的转换然后内部执行RunCode会转变为一个const int类型参与RunCode的匹配过程。这里static_castT折叠后变为static_castconst int这里的右值引用之所以不会当成作值类似std::move返回的右值引用是因为这里得到的右值引用是一个匿名的右值引用。所以参与函数传参时不会被当成左值。为了完整我们在main中再补充一组测试printf(tag\n); int aa a; const int caa c; int aaa move(a); const int caaa move(d); PerfectForward(aa); PerfectForward(caa); PerfectForward(aaa); PerfectForward(caaa);上述补充内容对应的输出为因为这里的aaacaaa是有名字的右值引用所以参与函数传参时被当作左值对待了。

相关新闻