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