C++ 引用折叠(Reference Collapsing)的具体使用
作者:点云SLAM
一句话定义
当“引用的引用”在模板、typedef / using、auto、decltype 中出现时,
编译器按照固定规则把它折叠为单一引用类型。
一、为什么需要引用折叠?
C++ 语法层面 不允许显式写:
int&&& x; // 非法
但在模板推导后,这种“引用的引用”会隐式产生:
template<typename T> void f(T&& x);
当这样调用:
int a; f(a); // T = int&
于是参数类型变成:
T&& → int& && ← 出现“引用的引用”
引用折叠规则的存在目的:
让模板推导在语法上始终合法
二、唯一的折叠规则
C++ 标准中的核心规则
只要出现左值引用 &,最终结果就是 &
只有 && && 才会折叠成 &&
四种情况
| 原始形式 | 折叠结果 |
|---|---|
| T& & | T& |
| T& && | T& |
| T&& & | T& |
| T&& && | T&& |
口诀版
“& 是霸道的,只要出现就赢”
三、引用折叠只会在这些地方发生
不是任何地方都会发生引用折叠
会发生的场景
- 模板类型推导
- using / typedef
- auto
- decltype
- std::forward / 完美转发
不会发生的场景
int&& && x; // 语法错误(非模板上下文)
四、模板推导中的引用折叠(最重要)
1 万能引用(Forwarding Reference)
template<typename T> void f(T&& x);
调用情况分析
int a = 10; f(a);
推导过程:
T = int& T&& = int& && → 折叠 → int&
x 是 左值引用
f(10);
T = int T&& = int&&
x 是 右值引用
结论(非常重要)
T&& 在模板中 ≠ 右值引用
它是 万能引用(forwarding reference)
五、auto中的引用折叠
示例 1:auto&&
int a = 10; auto&& x = a;
推导:
auto = int& auto&& = int& && → int&
auto&& y = 10;
auto = int auto&& = int&&
结论
auto&& // 永远是万能引用
六、using / typedef中的引用折叠
示例
using LRef = int&; using RRef = int&&; LRef& → int& LRef&& → int& RRef& → int& RRef&& → int&&
using 不会阻止引用折叠
七、decltype+ 引用折叠(最容易踩坑)
规则回顾
int x = 10; decltype(x) // int decltype((x)) // int& ← 注意括号!
示例
decltype((x))&& y = x;
推导:
decltype((x)) = int& int& && → int&
结论
decltype 的结果本身可能带引用
再加 && 就会触发引用折叠
八、完美转发 = 引用折叠 + 值类别保持
std::forward的本质
template<typename T> T&& forward(remove_reference_t<T>& param);
使用示例
template<typename T>
void wrapper(T&& arg) {
foo(std::forward<T>(arg));
}
左值情况
T = int& forward<int&>(arg) → int&
右值情况
T = int forward<int>(arg) → int&&
引用折叠是完美转发成立的核心机制
九、常见错误 & 工程级坑点
误以为T&&一定是右值引用
template<typename T> void f(T&& x); // 错
正解:
当且仅当 T 是被推导出来的,T&& 才是万能引用
在非模板中使用&&期望折叠
void f(int&&&& x); //
忘了decltype((x))是引用
十、编译器视角总结
引用折叠不是运行期行为
它发生在模板实例化 + 类型替换阶段
编译流程中位置
模板推导 → 生成候选类型 → 引用折叠 → 最终参数类型 → 代码生成
十一、终极总结
1️⃣ 引用折叠只在模板/类型推导中发生
2️⃣ 规则只有一条:有 & 就是 &
3️⃣ T&& + 模板推导 = 万能引用
4️⃣ 完美转发的底层机制 = 引用折叠
5️⃣ decltype((x)) 是最常见陷阱
Eigen/SLAM 中:引用折叠如何避免拷贝
一、Eigen 场景中的真实问题
在 SLAM 代码中经常会写:
Eigen::Matrix<double, 15, 15> H; Eigen::Matrix<double, 15, 1> b;
或者:
Eigen::Vector3d r; Eigen::Matrix3d J;
天真的接口(会拷贝)
void AddResidual(Eigen::VectorXd r) {
// 每次调用都会拷贝
}
Eigen 动态矩阵 → 昂贵拷贝
二、Eigen 官方的解决方案:模板 + 表达式
Eigen 的核心思想
Matrix = 表达式
表达式不是值,而是 Expression Template
三、错误写法 vs 正确写法(关键对比)
错误:值传递
template<typename Derived>
void AddResidual(Eigen::MatrixBase<Derived> r) {
// 拷贝已经发生
}
错误:const 引用但阻断右值
template<typename Derived> void AddResidual(const Eigen::MatrixBase<Derived>& r);
- 不能高效接收临时表达式
- 失去 move / lazy evaluation 的机会
四、Eigen 推荐的“零拷贝”接口写法
核心模式(引用折叠的舞台)
template<typename Derived>
void AddResidual(Eigen::MatrixBase<Derived>&& r) {
// 完美接收左值 or 右值
}
这是 Eigen 内部大量使用的模式
五、引用折叠在这里如何工作?
情况 1:传左值(已有残差向量)
Eigen::Vector3d r; AddResidual(r);
推导过程
T = Eigen::Vector3d& T&& = Eigen::Vector3d& && → 折叠 → Eigen::Vector3d&
r 以 左值引用 进入
无拷贝
情况 2:传右值表达式(Eigen 的精华)
AddResidual(J * dx + r0);
这里:
J * dx + r0 → Eigen::CwiseBinaryOp<...>(表达式)
推导过程
T = CwiseBinaryOp<...> T&& = CwiseBinaryOp<...>&&
表达式对象 零拷贝传入
计算 延迟到函数内部
六、为什么const&在 Eigen 中不够好?
问题
const Eigen::MatrixBase<Derived>& r
- 强制绑定 const
- 阻止某些 move / eval
- 某些表达式不能安全延迟
Eigen 官方风格
template<typename Derived> void foo(Eigen::MatrixBase<Derived>&& x);
并在内部:
auto&& expr = std::forward<Derived>(x);
七、SLAM 后端:残差 & Jacobian 的真实例子
一个真实的误差项接口
template<typename ResidualDerived, typename JacobianDerived>
void AddFactor(ResidualDerived&& r,
JacobianDerived&& J) {
using RType = std::remove_reference_t<ResidualDerived>;
using JType = std::remove_reference_t<JacobianDerived>;
// 仅在必要时 eval
const RType& r_eval = r;
const JType& J_eval = J;
// 参与正规方程
}
调用方式(零拷贝)
AddFactor(
J * dx + r0, // 右值表达式
J.transpose() * J // 右值表达式
);
八、结合李代数(SO(3) / SE(3))
常见 SLAM 代码
template<typename TangentDerived>
void ApplyUpdate(TangentDerived&& delta) {
// delta 是 Eigen::Matrix<double, 6, 1> 或表达式
xi_ = Sophus::SE3d::exp(delta) * xi_;
}
左值情况
Eigen::Matrix<double, 6, 1> dx; ApplyUpdate(dx);
→ delta 折叠为 Eigen::Matrix<...>&
右值情况
ApplyUpdate(H.ldlt().solve(b));
→ delta 是 Eigen 表达式
→ 无临时矩阵拷贝
九、std::forward 在 Eigen / SLAM 中的作用
正确姿势
template<typename Derived>
void Foo(Derived&& x) {
Bar(std::forward<Derived>(x));
}
错误姿势
Bar(x); // 丢失值类别 → 右值变左值
十、性能视角总结(非常重要)
| 写法 | 是否拷贝 | 表达式延迟 | 工程推荐 |
|---|---|---|---|
| 值传递 | ❌ | ❌ | 禁用 |
| const& | ✔ | 部分 | 一般 |
| T&& + forward | ✔✔ | ✔✔ | ⭐⭐⭐ |
十一、Eigen / SLAM 模板黄金法则
任何可能接收 Eigen 表达式 / 李代数增量的接口:
template<typename T> void func(T&& x);
并在内部:
auto&& v = std::forward<T>(x);
十二、在 SLAM 系统中应该立刻用的模式
后端残差
template<typename R, typename J>
void AddResidual(R&& r, J&& J) { ... }
状态更新
template<typename DX> void Update(DX&& dx);
图优化 factor 构造
template<typename Measurement> Factor(Measurement&& z);
总结
Eigen 的高性能 = 表达式模板
表达式模板的生命线 = 引用折叠 + 完美转发
到此这篇关于C++ 引用折叠(Reference Collapsing)的具体使用的文章就介绍到这了,更多相关C++ 引用折叠内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
