ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++11右值引用与完美转发及可变参数模板详解

C++11右值引用与完美转发及可变参数模板详解 前言C11 引入的右值引用rvalue reference是语言层面的一次值类别革命。它不仅带来了移动语义还派生出了一个几乎无处不在的语法现象T在模板里不是右值引用而是转发引用forwarding reference。配合引用折叠reference collapsing规则和std::forward就构成了完美转发perfect forwarding。而可变参数模板variadic template则是另一条线索它让参数个数任意这件事第一次能用类型安全的方式表达而不用再写printf那样的...加va_list。这两者合在一起才有了今天的std::make_unique、emplace_back、std::thread、std::tuple—— 所有接受任意参数、原样转发给另一个函数的设施。本文会从值类别这个根上讲起一路推到完美转发和可变参数模板的实现最后用一个能跑的手写make_unique收尾。一、值类别左值、纯右值、将亡值C11 之前只有左值和右值两个概念划分标准是能不能取地址。C11 把它扩展成三个类别英文直观含义例子左值lvalue有名字、有确定地址的对象int a;中的a纯右值prvalue (pure rvalue)纯计算的临时结果1 2、std::string(x)将亡值xvalue (eXpiring value)即将生命终结、资源可被窃取的对象std::move(a)、static_castT(a)在讨论引用绑定时我们把它们归成两类泛左值glvalue, generalized lvalue 左值 将亡值右值rvalue 纯右值 将亡值为什么要分 xvalue因为能不能被移动这个判断需要一个精确的语义位置std::move(a)并不移动任何东西它只是把a转换成将亡值从而让重载决议选中接受T的函数。真正执行资源窃取的是那个函数。template class T typename std::remove_referenceT::type move(T t) noexcept { return static_casttypename std::remove_referenceT::type(t); }注意std::move没有任何运行期开销它是一次纯粹的类型转换。二、右值引用Tint i 42; int lr i; // 左值引用 int rr 42; // 右值引用绑定到右值 int rr2 std::move(i); // 绑定到 i现在 i 是将亡值语义关键认知具名的右值引用本身是左值。void foo(int x) { // 在这里x 是一个左值它有名字可以取地址 int* p x; // ✅ 合法 bar(x); // ⚠️ 调用的是 bar(int)不是 bar(int) bar(std::move(x)); // ✅ 要转移资源必须再 move 一次 }这一条是理解完美转发失败的根源也是为什么需要std::forward的答案。三、引用折叠与转发引用3.1 引用折叠规则C 不允许引用的引用但在模板类型推导时会出现这种情况标准规定按下面规则折叠组合折叠结果T TT TT TT T一句话概括只要出现一个左值引用结果就是左值引用只有两个都是右值引用结果才是右值引用。3.2 转发引用forwarding referencetemplate class T void f(T param); // 这是转发引用当模板参数推导发生在T上时传入左值 →T推导为U→U 折叠为U左值引用传入右值 →T推导为U→U右值引用同一条f定义既能接左值又能接右值而且保留值类别。这就是转发引用名字的来源。它必须满足两个条件才是转发引用这一点被非常多人搞错形式必须是T且T是当前模板函数自己的模板参数。不能有任何 cv 限定或其它修饰干扰推导。❌ 都不是转发引用template class T void a(std::vectorT v); // ❌ 不是 T是右值引用 template class T void b(const T v); // ❌ 有 const不是转发引用 template class T void c(T v); // ✅ 是 template class T class C { void d(T v); // ❌ T 是类模板参数不参与本次推导是右值引用 };最后一条尤其隐蔽类模板的T不是转发引用因为T在实例化时就已经确定了。四、std::forward把值类别原样传下去template class T T forward(typename std::remove_referenceT::type arg) noexcept { return static_castT(arg); }它的作用是如果T被推导为左值引用类型U返回左值引用否则返回右值引用。配合引用折叠正好还原了调用者传入时的值类别。4.1 一个完美转发的完整例子// forward_demo.cpp — g -stdc11 forward_demo.cpp -o forward_demo #include iostream #include string #include utility void sink(std::string s) { std::cout 左值版本: s \n; } void sink(std::string s) { std::cout 右值版本: s \n; } template class T void relay(T arg) { sink(std::forwardT(arg)); // 原样转发值类别 } int main() { std::string s hello; relay(s); // 输出左值版本 relay(std::string(world)); // 输出右值版本 relay(std::move(s)); // 输出右值版本 }如果把std::forwardT(arg)换成arg三次调用会全部走左值版本——因为具名的arg是左值。如果换成std::move(arg)三次会全部走右值版本包括本该是左值的那次——这可能导致调用方对象被意外掏空是比前者更危险的 bug。4.2std::move与std::forward的选用原则场景用哪个理由转发引用参数T要传给下游std::forwardT(x)保留调用者的值类别已知对象不再使用要转移资源std::move(x)无条件转成右值函数返回局部对象不用写编译器自动做 NRVO/隐式移动返回值可能是右值引用参数std::forwardT(x)例如std::optional的value()最后一条值得展开从一个可能是右值引用的参数返回时绝不能用std::move因为那会对左值引用也强行移动template class T T wrapper(T x) { return std::forwardT(x); // ✅ 保留值类别 // return std::move(x); // ❌ 即使传入的是左值也会被当成右值 }五、可变参数模板variadic template5.1 语法基础template class... Args // 类型参数包template parameter pack void func(Args... args); // 函数参数包 template class... Args void func(Args... args); // 万能引用 参数包三个关键操作sizeof...(Args)包中元素个数Args...包展开pack expansion递归或折叠表达式fold expressionC17拆包5.2 C11 的递归展开C11 没有折叠表达式只能用递归 终止重载来拆包#include iostream // 终止条件 void print() { std::cout \n; } template class T, class... Rest void print(const T first, const Rest... rest) { std::cout first; if (sizeof...(rest) 0) std::cout , ; print(rest...); // 递归每次剥掉一个参数 } int main() { print(1, 2.5, three, c); // 输出1, 2.5, three, c }5.3 C17 的折叠表达式template class... Args auto sum(Args... args) { return (args ...); // 一元右折叠 } template class... Args void printAll(const Args... args) { ((std::cout args ), ...); // 逗号折叠 }折叠表达式消灭了递归和终止重载编译更快、生成的代码更短。但如果你的目标是 C11 兼容递归是唯一的办法。5.4 可变参数模板的实际用途emplace_back把参数原地构造成容器元素省掉临时对象。std::make_unique/std::make_shared把参数完美转发给构造函数。std::thread把可调用物和参数都转发到新线程。std::tuple任意类型任意数量的聚合。类型安全的变参日志。六、代码实战手写make_unique下面这个例子把本文所有知识点串起来转发引用、引用折叠、std::forward、可变参数模板、完美转发。// my_make_unique.cpp — g -stdc11 my_make_unique.cpp -o my_make_unique #include iostream #include memory #include string #include utility #include type_traits template class T, class... Args std::unique_ptrT myMakeUnique(Args... args) { // new T(...) 中args 必须用 forward 原样转发否则多一次拷贝甚至编译失败 return std::unique_ptrT(new T(std::forwardArgs(args)...)); } // 数组版本C11 里 unique_ptrT[] 需要单独处理 template class T std::unique_ptrT myMakeUniqueArray(std::size_t n) { return std::unique_ptrT(new typename std::remove_extentT::type[n]()); } struct User { std::string name; int age; std::string tag; User(std::string n, int a, std::string t) : name(std::move(n)), age(a), tag(std::move(t)) { std::cout User 构造: name \n; } User(const User o) : name(o.name), age(o.age), tag(o.tag) { std::cout User 拷贝构造: name \n; } }; int main() { std::string n alice; // 传左值 nforward 保持左值 → 触发一次拷贝进 User::n auto u1 myMakeUniqueUser(n, 30, admin); // 传临时 stringforward 保持右值 → 走移动 auto u2 myMakeUniqueUser(std::string(bob), 25, std::string(guest)); std::cout u1-name u1-age u1-tag \n; std::cout u2-name u2-age u2-tag \n; auto arr myMakeUniqueArrayint[](8); arr[0] 7; std::cout arr[0] arr[0] \n; return 0; }实验建议把std::forwardArgs(args)...改成args...不加 forward你会看到User 拷贝构造多打印一次——因为args是左值User的std::string成员只能靠拷贝构造。改回forward后拷贝消失。这就是完美转发在性能上的直接体现。6.1 转发引用在类型推导中的陷阱实验#include type_traits #include iostream template class T void deduce(T x) { std::cout T is lvalue ref: std::is_lvalue_referenceT::value \n; } int main() { int a 1; deduce(a); // T int → is_lvalue_referenceT 1 deduce(1); // T int → is_lvalue_referenceT 0 }这段代码直观展示了引用折叠是怎么在类型系统里落地的。理解它你就能预测任意一个转发引用模板在传入不同实参时的T是什么。常见坑点坑点 1在转发函数中多次使用std::forward❌ 错误写法template class T void badForward(T x) { log(x); // 读一次 OK consume(std::forwardT(x)); // 把 x 移走了 log(x); // ❌ UB 风险x 可能已被掏空 }✅ 正确写法std::forward只能对每个对象用一次且必须是最后一次使用。template class T void goodForward(T x) { log(x); // 先做只读操作 refresh(x); // 其它处理 consume(std::forwardT(x)); // 最后才转发 }坑点 2对左值使用std::move导致调用方对象被掏空❌ 错误写法std::vectorstd::string cache; void save(const std::string s) { cache.push_back(s); } template class T void saveWrapper(T s) { save(std::move(s)); // ❌ 传左值时会把调用方的 string 搬空 } int main() { std::string name important; saveWrapper(name); std::cout name; // 可能是空字符串未指定行为 }✅ 正确写法用std::forward让值类别由调用者决定。save(std::forwardT(s)); // ✅ 左值走拷贝右值走移动坑点 3返回std::move(局部变量)❌ 错误写法std::string make() { std::string s abc; return std::move(s); // ❌ 阻止了 NRVO反而更慢 }标准规定返回局部对象时编译器可以在原地构造NRVO或者至少隐式移动。加上std::move会把返回值变成std::string破坏了 NRVO 的条件导致多一次移动构造。✅ 正确写法std::string make() { std::string s abc; return s; // ✅ 自动 NRVO / 隐式移动 }例外如果返回的是函数参数或成员变量那么std::move是必要的因为它们不属于局部自动对象的 NRVO 范畴。坑点 4const T退化成拷贝template class T void f(const T x); // ❌ 不是转发引用const T有两个问题第一它不是转发引用T的推导规则不同第二即便推导出const U绑定到它的对象也是const的移动构造需要T非 const所以最终还是会走拷贝。✅ 正确写法写转发引用就老老实实写T不要加const。坑点 5可变参数模板的递归终止必须精确匹配❌ 错误写法template class T, class... Rest void print(const T first, const Rest... rest) { std::cout first; print(rest...); // ❌ 参数包为空时没有 print() 可调用 → 编译错误 }编译错误信息通常极其冗长error: no matching function for call to print() ...candidate template ignored: couldnt infer template argument T✅ 正确写法提供空参终止重载或者C17 起用if constexpr。// 方案 A终止重载 void print() {} // 方案 BC17 if constexpr template class T, class... Rest void print(const T first, const Rest... rest) { std::cout first; if constexpr (sizeof...(rest) 0) { std::cout , ; print(rest...); } }坑点 6sizeof...(Args)与sizeof(Args)...的巨大区别template class... Args void f(Args... args) { std::size_t n sizeof...(Args); // ✅ 参数个数 // std::size_t m sizeof(Args)...; // ❌ 语法错误 // std::size_t k sizeof...(args); // ✅ 也可用于函数参数包 }注意sizeof...(args)也是合法的——它计算的是参数包中元素个数而不是字节数。坑点 7完美转发在花括号初始化列表上失效template class T, class... Args std::unique_ptrT make(Args... args) { return std::unique_ptrT(new T(std::forwardArgs(args)...)); } struct P { P(int, int) {} }; // makeP({1, 2}); // ❌ 编译错误无法从 braced-init-list 推导 Args原因花括号初始化列表不是表达式没有类型模板参数推导无法处理。这是语言层面的限制。✅ 规避显式指定类型或者用std::initializer_list重载。auto p makeP(1, 2); // ✅ 直接给两个 int // 或者 template class T std::unique_ptrT makeInit(std::initializer_listint il);总结概念本质关键词值类别左值 / 纯右值 / 将亡值xvalue 是可被移动的语义标记std::move(x)无运行期开销的强制类型转换它不移动任何东西右值引用T能绑定到右值的引用具名的右值引用是左值引用折叠 → → 决定转发引用的推导结果转发引用模板函数中的T必须是模板自己的T不能有conststd::forwardT按T决定转成左值还是右值每个对象只能用一次可变参数模板类型安全的任意参数C11 靠递归C17 靠折叠表达式右值引用和完美转发的核心思想只有一句话让值类别这个信息在整个调用链上不丢失地传递下去。理解了这个目标std::move和std::forward的区别、转发引用为什么必须是T、为什么具名的右值引用是左值——这些看似零散的规则就都能自己推导出来了。
返回列表