引用折叠
概述
- 引用折叠(
Reference Collapsing)是C++11引入的机制,用于处理模板编程中多重引用组合的类型推导问题 - 当间接生成“引用的引用”(如通过模板参数推导、类型别名等)时,编译器会根据规则将其折叠为单一引用类型
- 它的核心目的是支持完美转发(
Perfect Forwarding)和万能引用(Universal Reference),确保参数的值类别(左值/右值)在传递过程中不被破坏
引用折叠的发生场景
- 模板实例化
- 模板函数或类的参数推导中,若模板参数为
T&&(万能引用),且传入左值或右值时,编译器通过类型推导生成可能的引用的引用,再触发折叠
- 模板函数或类的参数推导中,若模板参数为
|
1 2 3 4 |
template<typename T> void func(T&& param); int x = 10; func(x); // T推导为int& → 折叠为int& func(10); // T推导为int → 保持为int&& |
auto类型推导auto&&变量的初始化会根据初始值的类型触发折叠
|
1 2 |
auto&& r1 = x; // auto推导为int& → 折叠为int& auto&& r2 = 10; // auto推导为int → 保持为int&& |
- 类型别名与
typedef- 使用类型别名或
typedef间接生成引用的引用时
- 使用类型别名或
|
1 2 |
typedef int& LRef; LRef&& r = x; // 折叠为int& |
decltype表达式- 分析
decltype(v)时若出现引用的引用,也会触发折叠
- 分析
引用折叠的规则
- 引用折叠遵循左值优先原则,规则如下:
- 核心逻辑:
- 只要组合中存在左值引用(
T&),结果必为左值引用; - 仅当两者均为右值引用时,结果才是右值引用
- 只要组合中存在左值引用(
| 组合类型 | 折叠结果 | 解释 |
T& & |
T& |
左值引用 + 左值引用 → 左值引用 |
T& && |
T& |
左值引用 + 右值引用 → 左值引用 |
T&& & |
T& |
右值引用 + 左值引用 → 左值引用 |
T&& && |
T&& |
右值引用 + 右值引用 → 右值引用 |
引用折叠的实现原理
- 模板推导与折叠触发
- 在模板实例化时,若推导出
T为引用类型(如T = int&),则参数T&&会形成引用的引用(如int& &&),此时编译器自动应用折叠规则,生成最终类型(如int&)
- 在模板实例化时,若推导出
std::forward的实现- 完美转发的核心函数
std::forward依赖引用折叠: - 若
T为左值引用(Widget&),T&&折叠为Widget&,返回左值引用 - 若
T为非引用(Widget),T&&保持为Widget&&,返回右值引用
- 完美转发的核心函数
|
1 2 3 4 |
template<typename T> T&& forward(typename std::remove_reference<T>::type& param) { return static_cast<T&&>(param); } |
- 万能引用相关
- 见下节
疑问与理解
template<typename T> void func(T&& param);
func(10);//T推导为int→ 保持为int&&- 首先,
T&&在模板参数推导时被称为万能引用 - 其特殊性在于:
可绑定左值或右值:根据传入实参的值类别(左值/右值)推导T的类型
引用折叠规则:T&&的实际类型由推导出的T类型和折叠规则共同决定 - 调用
func(10)时,10是一个右值(字面量),触发模板参数推导 - 传入右值
10:
编译器根据万能引用的推导规则,将T推导为int(非引用类型)
万能引用的推导规则见下节万能引用的推导规则总结 T&&的具体类型由T的推导结果和引用折叠规则决定:
引用折叠规则见上节 引用折叠的规则
T推导为int→param的类型为int&&(无折叠发生)
- 首先,
template<typename T> void func(T&& param);
func(x);//T推导为int&→ 折叠为int&- 传入了左值
x:
编译器根据万能引用的推导规则,将T推导为int& T&&的具体类型由T的推导结果和引用折叠规则决定:
于是,T&&(T& &&)折叠为int&
- 传入了左值
- 显式指定模板参数
func<int&&>(10);//T被显式指定为int&&→param类型为int&&T&&直接为int&&,无推导过程
auto&& r1 = x;//auto推导为int&→ 折叠为int&- 当使用
auto&&声明变量时,auto的类型推导规则与模板参数推导中的万能引用(T&&)一致,具体规则见下节其他内容auto推导规则 - 在
auto&& r1 = x;中,x是一个左值变量,因此auto被推导为int& - 当
auto被推导为int&后,auto&&的表达式变为int& &&。此时触发引用折叠规则(规则见上节引用折叠的规则) - 这是一个经典的左值引用+右值引用的场景,折叠为左值引用
- 因此,
r1的最终类型为int&
- 当使用
万能引用
概述
- 万能引用(
Universal Reference,又名转发引用)是C++11引入的特殊引用类型,能够同时绑定左值和右值 - 其语法形式为
T&&,但仅在以下两种语境中成立:- 函数模板中:当模板参数
T涉及类型推导时,形如T&&的参数会被识别为万能引用 auto&&声明中:通过auto&&定义的变量可根据初始化值的左右值属性推导为左值或右值引用
- 函数模板中:当模板参数
特点
- 本质上是一种类型推导机制,结合引用折叠规则实现灵活绑定
- 与右值引用语法相同(
&&),但功能更广泛,右值引用仅能绑定右值,而万能引用可同时绑定左右值
实现原理与类型推导规则
- 万能引用的本质是模板类型推导与引用折叠的结合:
- 模板推导:
- 当传入左值时,
T被推导为T&,T&&折叠为T&(左值引用) - 当传入右值时,
T被推导为T,T&&保持为T&&(右值引用)
- 当传入左值时,
|
1 2 3 4 |
template<typename T> void func(T&& param); int x = 10; func(x); // T=int& → param=int& func(10); // T=int → param=int&& |
- 引用折叠规则:
- 见上节
万能引用推导退则总结
- 若实参为右值,
T推导为非引用类型(int) - 若实参为左值,
T推导为左值引用类型(如int&)
完美转发
概述
- 完美转发(
Perfect Forwarding)是C++11引入的关键特性,旨在解决泛型编程中参数传递的完整性与高效性问题
定义
- 函数模板能够将接收到的参数(包括左值、右值、常量属性等)无损地 传递给其他函数,保留其原始值类别(左值/右值)和类型属性(如
const修饰符)
实现机制:三大核心技术
- 万能引用
- 见上节
- 引用折叠
- 见上节
std::forward条件转发- 根据模板参数
T的类型,决定将参数转换为左值或右值引用 - 若
T为左值引用(Widget&),static_cast<T&&>折叠为Widget&,返回左值引用 - 若
T为非引用(Widget),static_cast<T&&>保持为Widget&&,返回右值引用
- 根据模板参数
|
1 2 3 4 |
template<typename T> T&& forward(typename remove_reference<T>::type& arg) { return static_cast<T&&>(arg); // 引用折叠生效 } |
应用场景
- 工厂函数与对象构造
std::make_unique、std::make_shared等通过完美转发直接调用构造函数,避免中间拷贝
|
1 2 3 4 |
template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args&&... args) { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); } |
- 容器操作优化
emplace_back直接在容器内存中构造元素,跳过分步构造和拷贝/移动操作
|
1 2 |
std::vector<Widget> vec; vec.emplace_back("Hello", 42); // 直接构造 Widget 对象 |
- 泛型包装器与中间层
- 代理模式或装饰器中转发参数到实际处理函数
|
1 2 3 4 |
template<typename F, typename... Args> auto wrapper(F&& f, Args&&... args) { return std::forward<F>(f)(std::forward<Args>(args)...); } |
- 委托构造函数
- 在类构造函数中复用其他构造逻辑,避免代码重复
auto推导规则
概述
auto是C++11引入的自动类型推导关键字,其核心目标是通过初始化表达式动态推断变量类型,减少冗余代码并提高可读性
基本推导规则
- 按值推导(非引用/非指针)
auto会忽略初始化表达式的引用和顶层 const/volatile,仅推导值类型
|
1 2 3 4 5 6 7 |
int x = 10; const int cx = x; const int& rx = x; auto a = x; // a → int(忽略顶层 const) auto b = cx; // b → int(忽略顶层 const) auto c = rx; // c → int(忽略引用和顶层 const) |
- 引用声明(
auto&或const auto&)- 显式添加引用时,
auto会保留底层 const和引用关系
- 显式添加引用时,
|
1 2 3 |
auto& d = x; // d → int& auto& e = cx; // e → const int& auto&& f = x; // f → int&(万能引用,引用折叠为左值引用) |
- 指针声明(
auto*)- 显式声明指针时,
auto保留指针类型和底层const
- 显式声明指针时,
|
1 2 3 |
const int* px = &x; auto* p1 = &x; // p1 → int* auto* p2 = px; // p2 → const int* |
- 万能引用(
auto&&)- 通过引用折叠规则推导,支持绑定左值或右值
|
1 2 3 |
int x = 10; auto&& r1 = x; // r1 → int&(左值) auto&& r2 = 42; // r2 → int&&(右值) |
特殊场景的推导规则
- 数组
- 按值推导退化为指针,按引用推导保留数组类型
|
1 2 3 |
int arr[3] = {1, 2, 3}; auto a = arr; // a → int* auto& b = arr; // b → int(&)[3] |
- 函数
- 按值推导退化为函数指针,按引用推导保留函数类型
|
1 2 3 |
void func(int); auto f1 = func; // f1 → void(*)(int) auto& f2 = func; // f2 → void(&)(int) |
- 初始化列表
- 使用
{}初始化时,auto推导为std::initializer_list<T>(C++11起)
- 使用
|
1 2 |
auto list1 = {1, 2, 3}; // list1 → std::initializer_list<int> auto list2{4}; // C++17 前为 initializer_list,C++17 起为 int |
- 直接列表初始化(
C++17)- 无等号的直接列表初始化(
auto x{value})在C++17中推导为值类型
- 无等号的直接列表初始化(
|
1 2 |
auto a{10}; // C++17 → int auto b = {10}; // → std::initializer_list<int> |
高级用法
decltype(auto)(C++14)- 保留表达式的完整类型(包括引用和修饰符)
|
1 2 3 4 5 |
int x = 10; int& get_ref() { return x; } auto a = get_ref(); // a → int(值拷贝) decltype(auto) b = get_ref(); // b → int&(保留引用) |
- 函数返回类型推导(
C++14)- 允许
auto推导函数返回类型
- 允许
|
1 2 3 |
auto add(int a, int b) { return a + b; // 返回类型 → int } |
- 结构化绑定(
C++17)- 简化对复杂类型的解构
|
1 2 |
std::pair<int, double> p{1, 3.14}; auto [num, val] = p; // num → int,val → double |
其他疑问与理解
void(*)(int)(函数指针)
- 指向函数的指针,存储函数的内存地址
|
1 2 |
void (*func_ptr)(int) = &some_function; // 显式取地址 void (*func_ptr2)(int) = some_function; // 函数名隐式转换为指 |
- 场景:运行时根据条件选择不同函数(如策略模式)
|
1 2 |
void (*handler)(int) = condition ? &funcA : &funcB; handler(42); |
void(&)(int)(函数引用)
- 函数的别名,直接绑定到函数实体,不涉及地址存储
|
1 |
void (&func_ref)(int) = some_function; // 必须绑定到现有函数 |
- 场景:避免拷贝函数对象,保留类型信息
|
1 |
void wrapper(void(&callback)(int)) { callback(42); } |
- 场景:配合
std::forward实现完美转发
|
1 2 |
template<typename F> void invoke(F&& f) { std::forward<F>(f)(42); } |
顶层 const
- 定义
- 表示对象本身是常量,不可修改
- 语法
- 直接修饰变量:
const int a = 10; - 修饰指针本身(指针地址不可变):
int* const ptr = &x; - 饰类成员函数中的
this指针(隐含):void func() const;
- 直接修饰变量:
- 特点
- 拷贝时忽略顶层
const
例如函数参数传递中不区分void f(int)和void f(const int)
- 拷贝时忽略顶层
|
1 2 |
int* const reg_ptr = 0x8000; // 指针地址不可变,但可通过指针修改寄存器值 *reg_ptr = 1; |
|
1 2 3 4 |
class Data { public: int get() const { /* this 指针是 const Data* const 类型 */ } }; |
- 特殊性
- 所有引用的
const都是底层const,因为引用一旦绑定后无法更改目标对象
- 所有引用的
- 拷贝
- 顶层
const被忽略:
- 顶层
|
1 2 |
const int a = 10; int b = a; // 合法,a 的顶层 const 被忽略 |
底层const
- 定义
- 表示指针或引用指向的对象是常量,不可通过该指针/引用修改其值
- 语法
- 指向常量的指针:
const int* ptr = &x; - 对常量的引用:
const int& ref = x;
- 指向常量的指针:
- 特点
- 在拷贝或传递时,底层
const必须严格匹配
例如不能将const int*赋值给int*
- 在拷贝或传递时,底层
|
1 |
void print(const std::string& s); // 通过底层 const 引用传递,不可修改 s |
|
1 |
const Base* ptr = new Derived(); // 通过底层 const 指针访问派生类 |
- 拷贝
- 底层
const必须匹配:
- 底层
|
1 2 |
const int* p1 = &a; int* p2 = p1; // 错误:底层 const 不匹配 |
- 类型转换
- 允许从非常量到常量的隐式转换(添加底层
const): - 禁止从常量到非常量的隐式转换(移除底层
const):
- 允许从非常量到常量的隐式转换(添加底层
|
1 2 |
int x = 10; const int* ptr = &x; // 合法 |
|
1 2 |
const int y = 20; int* p = &y; // 错误 |
const_cast 的安全性
- 移除底层
const时,若原对象本身是非const,则安全:
|
1 2 3 |
int x = 10; const int* p = &x; int* q = const_cast<int*>(p); // 合法,x 本身可修改 |
- 移除顶层
const(如const int→int)可能导致未定义行为
函数重载与 const 的关联
- 顶层
const无法区分重载函数(如void f(int)和void f(const int)冲突) - 底层
const可以区分重载(如void f(int*)和void f(const int*)构成重载)
|
1 2 3 4 5 |
void test(int x) {} void test(const int x) {} //严重性 代码 说明 项目 文件 行 禁止显示状态 详细信息 //错误 C2084 函数“void test(int)”已有主体 test1 D:\code_test\test1\test1\test1.cpp 7 |
decltype和auto
- 推导目标的差异
auto:
推导的是变量的 值类型,其核心逻辑与模板参数推导一致
它会忽略初始化表达式的 引用属性 和 顶层const/volatile修饰符,仅保留底层类型decltype:
推导的是表达式的 实际类型,包括引用、const/volatile修饰符和值类别(左值/右值)
它不会修改表达式的原始类型信息
|
1 2 3 4 |
const int ci = 10; auto a = ci; // a → int(忽略顶层 const) int& ri = ci; auto b = ri; // b → int(忽略引用属性) |
|
1 2 3 4 |
const int ci = 10; decltype(ci) d1 = ci; // d1 → const int int& ri = ci; decltype(ri) d2 = ri; // d2 → int& |
- 对引用和
const的处理auto:
默认丢弃引用和顶层const
若需保留引用,必须显式声明auto&或auto&&decltype:
完全保留表达式的引用和const属性
对于带括号的表达式(如(x)),会推导为左值引用
|
1 2 3 4 |
int x = 10; int& rx = x; auto a = rx; // a → int auto& b = rx; // b → int& |
|
1 2 3 |
int x = 10; decltype(x) d1 = x; // d1 → int decltype((x)) d2 = x; // d2 → int&(括号触发左值引用推导) |
- 初始化要求的区别
auto:必须初始化变量,因为其类型依赖于初始化表达式的值类型decltype:无需初始化变量,仅依赖表达式的类型信息
|
1 2 |
auto x = 42; // 合法 auto y; // ❌ 错误:未初始化 |
|
1 2 |
int x = 10; decltype(x) y; // 合法,y → int(类型已知,无需初始化) |
- 表达式敏感性的差异
auto:仅关注初始化表达式的最终类型,不分析表达式结构decltype:对表达式的结构和上下文敏感:
若表达式是 变量名,推导其声明类型(包括引用和const)
若表达式是 函数调用,推导函数返回类型(包括引用)
若表达式是 左值或带括号的表达式,推导为左值引用
|
1 2 |
int x = 0; auto a = x + 5.0; // a → double(表达式结果为 double) |
|
1 2 3 4 |
int x = 0; decltype(x) d1 = x; // d1 → int decltype((x)) d2 = x; // d2 → int&(括号触发左值引用) decltype(std::move(x)) d3 = 0; // d3 → int&&(右值引用) |
- 计算表达式结果
auto是“值类型推导工具”,需初始化并计算表达式结果decltype是“静态类型查询工具”,仅分析表达式类型结构
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Spdlog记述:三07/23
- ♥ C++_指针引用09/19
- ♥ COM组件_303/07
- ♥ breakpad记述:Windows下静态库的编译使用03/15
- ♥ C++标准库_cfenv02/14
- ♥ STL_了解05/02