C++ 引用折叠(Reference Collapsing)和示例讲解说明

C++ 引用折叠(Reference Collapsing)

一句话定义

当"引用的引用"在模板、typedef / usingautodecltype 中出现时,
编译器按照固定规则把它折叠为单一引用类型


一、为什么需要引用折叠?

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&&

口诀版

"& 是霸道的,只要出现就赢"


三、引用折叠只会在这些地方发生

不是任何地方都会发生引用折叠

会发生的场景

  1. 模板类型推导
  2. using / typedef
  3. auto
  4. decltype
  5. std::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));

deltaEigen 表达式

无临时矩阵拷贝


九、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 的高性能 = 表达式模板
表达式模板的生命线 = 引用折叠 + 完美转发

相关推荐
chenyuhao20242 小时前
Linux网络编程:HTTP协议
linux·服务器·网络·c++·后端·http·https
Minecraft红客2 小时前
ai_dialogue_framework项目1.0(纯原创)
c++·测试工具·电脑
挖矿大亨3 小时前
C++中的赋值运算符重载
开发语言·c++·算法
Liu-Eleven3 小时前
Qt/C++开发嵌入式项目日志库选型
开发语言·c++·qt
qq_433554543 小时前
C++区间DP
c++·算法·动态规划
saber_andlibert3 小时前
【C++转GO】文件操作+协程和管道
开发语言·c++·golang
历程里程碑3 小时前
滑动窗口解法:无重复字符最长子串
数据结构·c++·算法·leetcode·职场和发展·eclipse·哈希算法
星火开发设计4 小时前
广度优先搜索(BFS)详解及C++实现
数据结构·c++·算法··bfs·宽度优先·知识
@卞4 小时前
排序算法(3)--- 交换排序
数据结构·算法·排序算法