在 C++ 中,decltype 是 “declare type(声明类型)” 的缩写,用来在编译阶段查询一个变量或表达式的类型。
前面我们使用 auto 关键字定义变量时,auto 必须根据等号右边的初始值来推导类型。如果我们只是想 “借用” 某个已有变量或算式的类型来定义新变量,并不想拿那个算式的值来做初始化,就可以使用 decltype 来实现。
C++ decltype 的使用
在 C++ 中,使用 decltype 时只需要把变量或表达式放进小括号里,编译器就会在编译阶段推导出它的类型,随后我们就可以直接用这个类型来定义新变量。
语法:
decltype(表达式) 变量名;
decltype(表达式) 变量名 = 初始值;说明:
与 auto 不同,当 decltype 推导出的是普通类型时,我们既可以在定义变量时直接赋初值,也可以先声明、后面再赋值。
需要注意的是,编译器只是在编译阶段分析小括号里表达式的结果类型,在程序真正运行时并不会执行括号里的算式。
示例 1:推导变量与算式类型
#include <iostream>
int main() {
int a = 10;
decltype(a) b; // 推导为 int
b = 20;
decltype(a + 3.5) c = 12.5; // 推导为 double
decltype(a++) d = 30; // 仅推导类型
std::cout << "a = " << a << '\n';
std::cout << "b = " << b << '\n';
std::cout << "c = " << c << '\n';
std::cout << "d = " << d << '\n';
return 0;
}运行结果如下。
a = 10
b = 20
c = 12.5
d = 30分析:
decltype(a) 推导出的类型是 int,因此变量 b 可以像普通整数一样先声明、后赋值。算式 a + 3.5 的计算结果是 double 类型,所以变量 c 的类型就是 double。
再看 decltype(a++) d = 30; 这一句代码。从运行结果可以看出,a 的值依然是 10,并没有变成 11。这是因为 decltype 只在编译阶段查看 a++ 的结果类型,并不会在程序运行时真正执行自增操作。
C++ decltype 的推导规则
除了推导普通变量和算式的类型之外,decltype 还有两条与 auto 明显不同的推导规则,需要小伙伴们重点掌握。
1. 保留 const 与引用属性
当 decltype 小括号里直接放一个变量名时,这个变量原本声明的是什么类型,推导出来就是什么类型,它原有的 const 只读限定和引用属性都会被完整保留下来。下面我们来看一个例子。
示例 2:保留 const 与引用属性
#include <iostream>
int main() {
const int x = 100;
const int& ref = x;
auto m = ref; // 推导为 int
m = 200; // 普通变量可修改
decltype(x) y = 300; // 推导为 const int
decltype(ref) z = y; // 推导为 const int&
std::cout << "m = " << m << '\n';
std::cout << "y = " << y << '\n';
std::cout << "z = " << z << '\n';
return 0;
}运行结果如下。
m = 200
y = 300
z = 300分析:
在这个例子中,ref 的类型是 const int&。当我们执行 auto m = ref; 时,auto 会自动忽略掉原有的引用和顶层 const 限定。因此 m 只是一个普通的 int 变量,后面可以把它的值修改为 200。
而使用 decltype(x) 和 decltype(ref) 时,原有的 const 和引用属性都被完整保留了下来。由于 y 的实际类型是 const int,如果我们在定义时不赋初值,或者后续尝试修改它的值,都会导致编译报错。
decltype(x) y2; // 常量必须初始化
y = 400; // 不能修改常量2. 双层括号推导为引用
在 decltype 中,给变量名套一层括号和套两层括号,推导出来的结果完全不同:
- decltype(a):单层括号里只是单纯的变量名,直接按变量
a最初声明的类型推导,结果是int。 - decltype((a)):多套一层括号后,内层的
(a)会被当成一个表达式来分析,推导结果会变成引用类型int&。
为什么同样是表达式,前面示例 1 里的 a + 3.5 推导出来是普通的 double,而这里的 (a) 却会变成引用呢?其实区别就在于表达式的结果是不是左值。
像 a + 3.5 算出的是一个临时结果,没有固定的内存位置,因此会推导为普通类型 double。而变量 a 在内存中有固定的存储空间,属于左值,所以 (a) 作为左值表达式就会被推导为 int&。同理,像指针解引用 *p、数组元素 arr[0] 这类能放在等号左边赋值的左值表达式,使用 decltype 推导出来也都是引用类型。
平时写代码时,小伙伴们只需要记住一条直观的规律:“单层括号看原变量声明,双层括号一律变引用。” 下面我们通过一个示例来验证一下。
示例 3:单层与双层括号的区别
#include <iostream>
int main() {
int a = 10;
decltype(a) b = a; // 推导为 int
decltype((a)) c = a; // 推导为 int&
b = 50;
std::cout << "修改 b 后,a = " << a << '\n';
c = 99;
std::cout << "修改 c 后,a = " << a << '\n';
return 0;
}运行结果如下。
修改 b 后,a = 10
修改 c 后,a = 99分析:
decltype(a) b = a; 只有一层括号,b 是独立的 int 变量,修改 b 不会影响 a。而 decltype((a)) c = a; 套了双层括号,推导出的类型是 int&。此时 c 成了 a 的引用别名,把 c 改成 99 后,a 也会跟着变成 99。
下面代码中,n 的最终值是 25,小伙伴们可以思考一下为什么。
int n = 20;
decltype((n)) r = n;
r += 5;注意 只要在变量名外面套了双层括号,decltype((变量名)) 推导出的结果就一定是引用类型。既然是引用,定义时就必须绑定到一个已有的变量上,漏写初始值或者直接赋字面量常数都会导致编译报错。
推导函数模板的返回类型
在编写函数模板时,有时函数的返回类型取决于两个不同类型参数的运算结果,比如一个参数是 int,另一个参数是 double。这时我们就可以借助 decltype(a + b) 来指定函数的返回类型。
示例 4:推导模板返回类型
#include <iostream>
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
int main() {
auto res1 = add(10, 2.5);
auto res2 = add(10, 20);
std::cout << "res1 = " << res1 << '\n';
std::cout << "res2 = " << res2 << '\n';
return 0;
}运行结果如下。
res1 = 12.5
res2 = 30分析:
看到 auto add(T a, U b) -> decltype(a + b) 这行声明时,为什么不直接把 decltype(a + b) 写在函数名 add 的最前面呢?
原因其实很简单,编译器是按照从左往右的顺序读取代码的。如果我们把 decltype(a + b) 写在最开头,编译器读到 a + b 时还没有看见后面的参数列表 (T a, U b),根本不知道 a 和 b 是什么,就会直接报错。
因此 C++ 提供了这种 “尾置返回类型” 语法:先在函数开头写一个 auto 占位,等参数列表声明完之后,再通过箭头 -> decltype(a + b) 补上真正的返回类型。
到了 C++14 及以上标准,我们还可以使用更简短的 decltype(auto) 语法,让编译器直接按照 decltype 的规则推导 return 语句的返回类型。对于这个例子来说,下面 2 种写法是等价的。
// 写法 1
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
// 写法 2(推荐)
template <typename T, typename U>
decltype(auto) add(T a, U b) {
return a + b;
}其中,写法 2 省去了末尾的 -> decltype(a + b),代码更加简短直观。而且当 return 返回引用时,它也能完整保留引用属性,因此在实际开发中我们更推荐使用写法 2。
auto 与 decltype 的区别
auto 和 decltype 都是编译期类型推导关键字,但在推导依据和属性保留上有以下 3 点主要区别:
是否必须赋初值:auto 必须根据等号右边的初始值来推导类型,定义变量时必须初始化。decltype 则是根据小括号里的表达式推导类型,推导出普通类型时可以先声明、后赋值。
是否保留 const 与引用:auto 按值定义变量时会自动忽略原有的引用和顶层 const 限定。decltype(变量名) 则会完整保留变量声明时的 const 和引用属性。
日常选型建议:平时定义局部变量并赋初值时,我们直接使用 auto 最省事。只有在需要借用某个表达式的类型,或者需要通过 decltype(auto) 完整保留返回值的 const 与引用属性时,我们才使用 decltype。
