# C++ decltype的使用
## decltype概念
`decltype` 被称为类型说明符,它的作用是选择并返回操作数的数据类型,它的出现主要是解决复杂的类型声明。随着程序越来越复杂,程序中用到的类型也越来越多,这种复杂性体现在两个方面。
1. 一些类型难于“拼写”,它们的名字既难记又容易写错,还无法明确体现其真实目的和含义。
2. 有时候根本搞不清到底需要的类型是什么,程序员不得不回过头去从程序的上下文寻求帮助。
解决问题一,可以使用类型别名技术。
解决问题二,可以使用 `auto` 和本文的主题:`decltype`。
## decltype原理
`decltype` 并不会实际计算表达式的值,只会分析表达式并得到它的类型,函数调用也算一种表达式,因此不必担心在使用 `decltype` 时会真正的执行了函数。
## decltype + 变量
当使用 `decltype(var)` 的形式时,`decltype` 会直接返回变量的类型(包括顶层 `const` 和引用),不会返回变量作为表达式的类型。
```
const int ci = 0, &cj = ci;
// x的类型是const int
decltype(ci) x = 0;
// y的类型是const int &
decltype(cj) y = x;
```
`decltype` 加指针也会返回指针的类型,`decltype` 加数组,不负责把数组转换成对应的指针,所以其结果仍然是个数组,总之 `decltype(var)` 完美保留了变量的类型。
## decltype + 表达式
`decltype(expr)` 的结果根据 `expr` 的结果不同而不同:`expr` 返回左值,得到该类型的左值引用;`expr` 返回右值,得到该类型。
```
int ci = 0, cj = 0, *p = 0;
// 普通的算术表达式为右值,x的类型是int
decltype(ci + cj) x = 0;
// y的类型是int&
decltype(ci += cj) y = ci;
// *p解引用为左值,z的类型是int&
decltype(*p) z = ci;
```
但是 `decltype` 单独作用于对象,没有使用对象的表达式的属性,而是直接获得了变量的类型。要想获得变量作为表达式的类型,可以加一个括号:
```
int i = 42;
// 加了括号,变成了表达式
// 返回的是i的左值形式
// 因此ri的类型是int &
decltype((i)) ri = i;
int i = 42, *p = &i;
// temp的类型是int *&
decltype((p)) temp = p;
```
`decltype` 作用的是表达式,`(p)` 得到的是 `p` 的左值,所以 `temp` 一定是一个引用;`p` 是指向 `int` 类型的指针,因此 `decltype` 得到的是**指向 `int` 类型的指针的引用。**
## decltype + 函数
C++ 中通过函数的返回值和形参列表,定义了一种名为函数类型的东西。它的作用主要是为了定义函数指针。
例如:
```
// 声明了一个函数类型
using FuncType = int(int &, int);
// 下面的函数就是上面的类型
int add_to(int &des, int ori);
// 声明了一个FuncType类型的指针
// 并使用函数add_to初始化
FuncType *pf = add_to;
int a = 4;
// 通过函数指针调用add_to
int sum = pf(a, 2);
```
我们可以使用 `decltype` 获得函数 `add_to` 的类型:
```
decltype(add_to) *pf = add_to;
```
这样的声明就简单多了。
当使用 `decltype(func_name)` 的形式时,`decltype` 会返回对应的函数类型,不会自动转换成相应的函数指针。
## 总结
1. `decltype是为了解决复杂的类型声明而使用的关键字,称作decltype类型说明符。`
2. `decltype不会去真的求解表达式的值,可以放心使用。`
3. `decltype可以作用于变量、表达式及函数名。`
- 作用于变量直接得到变量的类型;
- 作用于表达式,结果是左值的表达式得到类型的引用,结果是右值的表达式得到类型;
- 作用于函数名会得到函数类型,不会自动转换成指针。
---
## 一、`decltype` 关键字
### 1. 基础概念
`decltype` 是 C++11 引入的编译期操作符,用于查询实体或表达式的声明类型。它的核心特点:
- **不会对表达式求值**,仅根据表达式的静态类型推导结果。
- 能够**完整保留类型的引用性、cv 限定符(const/volatile)**,这是 `auto` 做不到的。
语法:
```cpp
decltype(entity)
decltype(expression)
```
### 2. 推导规则(四类情况)
编译器根据括号内的表达式类别决定最终类型:
| 表达式形式 | 推导出的类型 | 示例 |
| ----------------------------- | ----------------------------------------- | ------------------------------------------------------------ |
| **无括号的标识符/类成员访问** | 实体被声明的类型(保留顶层 const 和引用) | `int x; decltype(x)` → `int`
`const int& cr = x; decltype(cr)` → `const int&` |
| **将亡值(xvalue)** | `T&&`,通常由移动语义产生 | `std::move(x)` → `int&&` |
| **左值(lvalue)** | `T&`,哪怕变量本身不是引用 | `int x; decltype((x))` → `int&`
`decltype(++x)` → `int&` |
| **纯右值(prvalue)** | `T`(非引用) | `decltype(42)` → `int`
`decltype(x + 0)` → `int` |
**关键记忆点:**
- 变量名不加括号 → 声明时的类型;
- 加括号(或用表达式生成左值) → 强制推导为左值引用。
### 3. 基础用法示例
#### (1) 声明与已知实体相同类型的变量
```cpp
const std::vector vec{1,2,3};
decltype(vec) another; // const std::vector
decltype(vec[0]) elem; // const int&(operator[] 返回 const int&)
```
#### (2) 尾置返回类型(Trailing Return Type)
在泛型编程中,返回类型依赖于模板参数时特别有用:
```cpp
template
auto add(T1 a, T2 b) -> decltype(a + b) {
return a + b;
}
```
#### (3) `decltype(auto)`(C++14)
专用于**完美转发返回类型**:用 `decltype` 规则推导返回值,保留引用性。
```cpp
template
decltype(auto) forwarder(T&& arg) {
return std::forward(arg);
}
// 若 arg 是左值,返回 T&;若是右值,返回 T&&。
```
与普通 `auto` 的区别:
- `auto` 会剥除引用和顶层 const(按值推导)。
- `decltype(auto)` 使用 `decltype` 的规则,保持原本的类型属性。
#### (4) 获取函数或数组类型(不发生退化)
```cpp
int arr[10];
decltype(arr) arr2; // int[10],不是 int*
void func();
decltype(func)* pf = func; // void(),需要加*才能声明指针
```
### 4. 注意事项
- **括号陷阱**:`int x=0; decltype(x) = int`,`decltype((x)) = int&`。如果 `decltype` 用于表达式且结果为左值,会引入引用,可能影响返回值优化或导致悬垂引用。
- **函数类型**:`decltype(func)` 得到的是函数类型而非函数指针,不可直接用于声明对象。
- **数组不退化成指针**:这点常用来在模板中保留数组长度信息。
- **与 SFINAE 结合时**:`decltype` 表达式内部如果产生非法的类型,会导致模板推导失败,这正是 SFINAE 的常用手法。
### 5. 拓展知识点
#### (1) 推导复杂表达式
`decltype` 可用于逗号表达式、条件运算符等:
```cpp
int a, double b;
decltype((a, b)) // double&(逗号表达式结果是右操作数的左值版本)
decltype(true ? a : b) // double&(条件表达式如果是左值,结果为左值引用)
```
#### (2) 在 lambda 中结合 `decltype(auto)`
泛型 lambda 的返回类型有时需要保留引用:
```cpp
auto lambda = [](auto&& x) -> decltype(auto) {
return std::forward(x);
};
```
#### (3) 辅助 SFINAE
利用 `decltype` 进行表达式合法性检测:
```cpp
template
auto has_size(int) -> decltype(std::declval().size(), std::true_type{});
template
std::false_type has_size(...);
```
---
## 二、`std::declval` 工具
### 1. 基础概念
`std::declval` 定义于 ``,是一个**编译期“假实例”函数**。它将任意类型 `T` 转换为**右值引用**(或引用折叠后的引用),以便在**不求值上下文**(unevaluated context)中“假装”拥有一个该类型的对象,从而调用成员函数、应用运算符等,而无需真正构造对象。
典型声明(C++11/14):
```cpp
template
typename std::add_rvalue_reference::type declval() noexcept;
```
注意:该函数只有声明,**没有定义**。调用它只允许出现在 `sizeof`、`decltype`、`noexcept` 等不实际执行代码的场合。
### 2. 核心用法
#### (1) 获取成员函数的返回类型
这是最常见的场景:你无法直接创建抽象类或无默认构造函数的对象,但可以用 `declval` 模拟调用。
```cpp
struct Foo {
double compute(int x) const;
};
using R = decltype(std::declval().compute(42)); // double
```
#### (2) 推断二元运算结果类型
```cpp
template
using sum_t = decltype(std::declval() + std::declval());
```
#### (3) SFINAE 中检测成员存在性(C++17 前经典手法)
配合 `void_t`:
```cpp
template
struct has_get_value : std::false_type {};
template
struct has_get_value().get_value())
>> : std::true_type {};
```
这里 `decltype(...)` 内部就是借助 `declval` 触发对 `get_value` 的查找。
### 3. 注意事项
- **只能用于不求值表达式**:如果在普通代码中调用 `declval`,会因找不到函数定义而**链接错误**。
- **不会产生真正的对象**:`declval` 返回引用,所以可以做 `declval().method()`,但不能进行值类别操作(如取地址),因为引用本身可以绑定,但背后没有真实对象。
- **对 `void` 的处理**:`std::declval()` 试图形成对 `void` 的引用,这是非法的。因此通常需要保护或概念约束。
- **引用折叠规则**:`declval` 返回 `T&&`(通过 `add_rvalue_reference`),若 `T` 本身就是左值引用 `U&`,则引用折叠后返回 `U&`。这保证了无论传左值引用还是右值引用类型,都能得到可绑定到成员函数的引用。
### 4. 拓展知识点
#### (1) 实现原理与 C++20 的变化
传统实现:
```cpp
template
typename std::add_rvalue_reference::type declval() noexcept;
```
C++20 中改为返回 `std::add_rvalue_reference_t` 并标记 `= delete`,防止误用,但核心思想不变。用户仍无法在求值环境中调用。
#### (2) 与 `std::result_of` / `std::invoke_result` 的关系
这些类型特征内部正是借助 `declval` 来模拟调用:
```cpp
// invoke_result 粗略实现:
template
struct invoke_result {
using type = decltype(std::declval()(std::declval()...));
};
```
#### (3) 修饰符的组合应用
- 可以模拟对象是 const:`std::declval()` 会得到 `const T&`,用于调用 const 成员函数。
- 可以模拟指针:`std::declval()`,再配合解引用等。
---
## 三、`decltype` 与 `declval` 的协同
两者天生就是搭档。典型模式:
```cpp
// 推断 T 类型的成员函数 foo() 的返回类型
using foo_return = decltype(std::declval().foo());
```
这种组合是编写泛型代码、类型萃取、SFINAE 表达式的基石。在现代 C++20 中,虽然概念(concepts)部分简化了此类检测,但 `decltype` + `declval` 仍在实现 `requires` 表达式内部以及底层库中广泛使用。
---
如果你能掌握:
- `decltype` 的推导规则(尤其是括号带来的左值引用化)
- `declval` 的“不求值假对象”哲学
- 两者结合在 SFINAE 和类型萃取中的应用
那么 C++ 类型推导领域的大部分难题都会迎刃而解。