C++ decltype

在 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。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号