`decltype` 是 C++11 引入的一种“编译期类型查询”机制,它能根据给定表达式的值类别(lvalue、xvalue、prvalue)精确推导出对应的类型,包括引用和 cv 限定;C++14 进一步引入了 `decltype(auto)`,可完美转发初始化器的值类别;在模板编程中,`decltype` 常用于后置返回类型与完美转发函数,以保持类型与引用语义;但要注意括号的影响及与 `auto` 的细微差别,以避免意外产生引用或丢失限定符。
# decltype 的基本语义
`decltype(expr)` 的类型推导遵循以下规则:
1. **无圆括号的标识表达式**:如果 `expr` 是无括号的变量名或成员访问,结果类型与该实体的声明类型完全一致(不添加引用)[Cppreference](https://en.cppreference.com/w/cpp/language/decltype?utm_source=chatgpt.com)。
2. **带圆括号或其他表达式**:
- 若 `expr` 是 lvalue,`decltype(expr)` 推导为 `T&`;
- 若为 xvalue,推导为 `T&&`;
- 若为 prvalue,推导为 `T` [cppreference.cn](https://cppreference.cn/w/cpp/language/decltype?utm_source=chatgpt.com)。
3. 操作数不求值,类似 `sizeof`,因此诸如 `decltype(i++)` 并不会改变 `i` 值 。
```cpp
auto varname = value;
decltype(exp) varname [= value];
// varname 表示变量名,value 表示赋给变量的值
// exp 表示一个表达式,方括号[ ]表示可有可无。
```
# 值类别与括号影响
- **括号陷阱**:
```cpp
int x = 0;
decltype(x) a; // a 的类型为 int
decltype((x)) b = a; // b 的类型为 int& (因为 (x) 被视作 lvalue 表达式)
```
过度或错误使用括号会导致意外引用类型。
- **函数调用与成员访问**:
```cpp
struct A { double x; };
const A* p = /*...*/;
decltype(p->x) m; // double
decltype((p->x)) n = m; // const double&(p 是 const A*,成员访问后再加圆括号)
```
成员访问时若带括号则变成 lvalue 引用类型。
# `decltype(auto)`(C++14)
C++14 引入了 `decltype(auto)`,用于变量或函数返回类型占位,可保留初始化表达式的值类别和 cv 限定:
```cpp
int i = 0;
const int ci = 1;
decltype(auto) v1 = i; // int
decltype(auto) v2 = (i); // int&
decltype(auto) v3 = ci; // const int
decltype(auto) v4 = (ci);// const int&
```
在函数模板中,`decltype(auto)` 可用于完美转发返回类型,避免普通 `auto` 丢失引用或限定符。
# 在模板与完美转发中的应用
`decltype` 在泛型编程中特别重要,常配合后置返回类型实现:
```cpp
template<typename T, typename U>
auto add(T&& t, U&& u) -> decltype(t + u) {
return t + u;
}
template<typename F, typename... Args>
decltype(auto) forward_call(F&& f, Args&&... args) {
return std::forward<F>(f)(std::forward<Args>(args)...);
}
```
- 后置返回类型使得函数返回类型依赖于模板参数表达式 。
- `forward_call` 示例中,`decltype(auto)` 保留函数返回值的引用语义,实现“完美转发。
# 常见陷阱与最佳实践
1. **避免多余括号**:过度括号会将无引用类型推为引用。
2. **区分 `auto` 与 `decltype(auto)`**:
- `auto` 总剥离引用与顶层 cv 限定;
- auto 必须要根据=右边的初始值 value 推导出变量的类型;所以auto要求变量必须初始化,也就是在定义变量的同时必须给它赋值,不然会编译报错。
- `decltype(auto)` 保留初始化表达式的值类别与 cv 限定。
- decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。 所以decltype 不强制要求初始化。
```cpp
#include <iostream>
int main() {
auto x = 5; //必须初始化,不然会报错,auto是根据初始值推导出变量的类型的
decltype(10)y; //可以不初始化,因为是根据()里面的内容推导出来的类型
decltype(10)z = 100; //也可以不初始化。
auto str1 = "decltype_test";
decltype(str1) str2 = "decltype_test2";
}
```
- cv 限定符
- decltype处理顶层const和引用的方式与auto有些不同,如果decltype使用的表达式是个变量,则decltype返回该变量的类型(包括顶层const和引用在内)。:
```cpp
#include <iostream>
int main(){
const int ci=0, &cj=ci;
decltype(ci) x=0; //x的类型是const int;
decltype(cj) y=x; //y的类型是const int &,y绑定到变量x;
decltype(cj) z; //错误,z是引用,必须初始化;
}
```
3. **使用 `decltype` 推导容器索引**:
推荐使用 `decltype(vec.size()) idx = 0;`,以防容器大小类型变化造成错误或警告。
4. **模板库中使用**:`decltype` 不会实例化表达式,所以可以安全地推导未定义时可能出错的类型,只要表达式本身的类型可在语法层面推导即可。