# 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++ 类型推导领域的大部分难题都会迎刃而解。