C++ 从入门到精通之十九
大纲
- C++ 从入门到精通之一、C++ 从入门到精通之二、C++ 从入门到精通之三
- C++ 从入门到精通之四、C++ 从入门到精通之五、C++ 从入门到精通之六
- C++ 从入门到精通之七、C++ 从入门到精通之八、C++ 从入门到精通之九
- C++ 从入门到精通之十、C++ 从入门到精通之十一、C++ 从入门到精通之十二
- C++ 从入门到精通之十三、C++ 从入门到精通之十四、C++ 从入门到精通之十五
- C++ 从入门到精通之十六、C++ 从入门到精通之十七、C++ 从入门到精通之十八
- C++ 从入门到精通之十九
C++ 进阶
decltype 类型推导说明符
核心概念
decltype 是 C++ 11 引入的类型推导说明符,用于在编译期获取实体或表达式的类型,而不对表达式进行求值。它与 auto 的主要区别在于类型推导规则不同:auto 通常按照类似函数模板实参推导的规则确定类型,而 decltype 对未加括号的变量名等特殊表达式采用实体的声明类型,对其他表达式则根据表达式的值类别确定类型,因此能够准确保留引用以及 const、volatile 等类型限定符。decltype 常用于泛型编程、尾置返回类型、类型萃取以及 SFINAE 等场景,是编写通用模板代码的重要工具。
适用场景
decltype的适用场景- 泛型编程中推导表达式类型:当表达式的类型依赖模板参数、难以手动指定时,可以使用
decltype获取表达式的类型。 - 尾置返回类型:当函数的返回类型依赖参数表达式时,可以使用
decltype推导返回类型。 - 完美转发:配合万能引用(转发引用)和
std::forward,根据模板参数的推导结果保留实参的值类别(左值 / 右值)。 - 保留 cv 限定符与引用:需要保留
const、volatile及引用类型时,可以使用decltype精确获取实体或表达式的类型。 - 声明与某实体或表达式具有相同类型的变量:例如
decltype(x) y = ...;,使y的类型按照decltype(x)的规则确定。 - 萃取函数返回类型:使用
decltype(f())获取函数调用表达式的类型,而不会实际调用函数。 - 类型萃取与模板元编程:使用
decltype获取表达式的类型,并将其用于类型别名、模板参数及其他类型计算。 - SFINAE 与类型检测:结合模板替换规则,利用
decltype检测表达式是否合法,例如检测某个类型是否支持指定的成员操作。
- 泛型编程中推导表达式类型:当表达式的类型依赖模板参数、难以手动指定时,可以使用
decltype的不适用场景- 需要获取表达式的运行结果:
decltype只用于编译期类型分析,不会对表达式进行求值,因此不能获取表达式的实际运行结果。 - 希望采用
auto的常规类型推导规则:如果希望通常忽略顶层const和引用,应考虑使用auto;但auto的具体推导行为仍取决于声明形式。 - 直接获取花括号初始化列表的类型:
decltype({1, 2, 3})不合法,因为花括号初始化列表不是表达式;应根据需求使用auto或显式指定类型。 - 直接获取函数指针类型:对于函数名
f,decltype(f)得到函数类型而不是函数指针类型;需要函数指针时,可以使用decltype(f)*。 - 希望自动进行数组到指针退化:
decltype(arr)会保留数组类型,例如int[3],不会自动退化为int*;需要时应显式进行相应的类型转换或声明。 - 进行运行时类型判断:
decltype用于编译期类型推导,不能获取运行时才确定的对象类型;需要运行时类型识别时,可根据场景考虑dynamic_cast或typeid。 - 使用未声明或不可访问的实体:相关名字必须满足名称查找、作用域及访问控制等规则,否则可能导致编译失败。
- 直接替代类型别名声明:
decltype本身不能替代using或typedef的命名作用,但可以配合它们定义类型别名。
- 需要获取表达式的运行结果:
推导规则
特别注意
以下案例代码都运行在 Windows 环境,并依赖了 Boost 库(主要是为了方便查看 auto 类型推导的结果,使用的 Boost 库版本是 1.92)。
作用于变量名
推导规则
decltype作用于变量名的推导规则:- 对未加括号的变量名使用
decltype是一种特殊情况,直接返回该变量的声明类型,完整保留const、volatile及引用类型等信息。 - 与
auto的关键区别:const int a = 10; auto x = a;中,auto按常规推导规则忽略顶层const,推导结果为int。const int a = 10; decltype(a) d = 0;中,decltype(a)保留变量a的声明类型,推导结果为const int。
- 变量名加上括号的区别:
decltype(x)→int(未加括号的变量名,直接返回声明类型)。decltype((x))→int&(加括号后的变量名属于普通表达式,x是左值,因此推导为左值引用)。- 注意:有名字的右值引用变量本身是左值表达式,因此
decltype((rr))的结果是左值引用类型int&,而不是右值引用类型int&&。
- 对未加括号的变量名使用
decltype作用于变量名的常见案例:变量声明 decltype使用示例推导结果 说明 int x;decltype(x)int未加括号的变量名,返回声明类型 const int a = 0;decltype(a)const int保留顶层 constvolatile int v;decltype(v)volatile int保留顶层 volatileconst volatile int cv = 0;decltype(cv)const volatile int同时保留 const和volatileint& r = x;decltype(r)int&保留左值引用类型 const int& b = a;decltype(b)const int&保留引用和 const限定符int&& rr = 0;decltype(rr)int&&保留右值引用类型 int arr[3];decltype(arr)int[3]保留数组类型,不退化为指针 int* p;decltype(p)int*返回指针类型 MyClass obj;decltype(obj)MyClass返回类对象的声明类型 int x;decltype((x))int&加括号后, x是左值表达式,推导为左值引用const int a = 0;decltype((a))const int&加括号后,保留 const并推导为左值引用int&& rr = 0;decltype((rr))int&有名字的右值引用变量是左值表达式,因此推导为左值引用
推导案例
1 |
|
程序运行的结果如下:
1 | decltype(c) = int |
作用于表达式
推导规则
decltype作用于表达式的推导规则:- 对于不属于未加括号的变量名等特殊情况的表达式,
decltype根据表达式的值类别进行推导:- (1) 若表达式是纯右值(prvalue),返回表达式的类型本身,不带引用。
- (2) 若表达式是左值(lvalue),返回
T&。 - (3) 若表达式是将亡值(xvalue),返回
T&&。
decltype的注意事项:- 对未加括号的变量名使用
decltype是一种特殊情况,直接返回变量的声明类型;而使用decltype((x))时,括号内的x是左值表达式,因此推导结果为int&。这就是decltype(x)与decltype((x))结果不同的原因。 decltype不会对表达式进行求值,因此不会执行表达式中的运行时操作。例如,decltype(x++)不会实际执行自增,decltype(x = 5)不会实际执行赋值,decltype(*p)也不会实际执行指针解引用。但这些表达式仍然必须满足相应的语法和语义要求,才能通过编译。
- 对未加括号的变量名使用
- 对于不属于未加括号的变量名等特殊情况的表达式,
decltype作用于表达式的常见案例:- 注意,在下面的表格内容中:
x、y和p假设分别为int类型的变量、int类型的变量和int*类型的指针;f()假设返回非引用类型。 - 涉及
++、--、赋值等操作时,以下结论针对内置运算符,重载运算符的行为可能不同。表达式类别 decltype使用示例推导结果 说明 左值 decltype((x))int&x是左值表达式,因此推导为左值引用左值 decltype(x = 5)int&内置赋值表达式的结果是左值 左值 decltype(++x)int&内置前置自增表达式的结果是左值 左值 decltype(*p)int&当 p是int*时,内置指针解引用表达式的结果是左值左值 decltype(true ? x : x)int&当 x是int左值时,条件表达式的结果也是左值左值 decltype((void)x, x)int&逗号表达式的结果具有最后一个操作数的值类别和类型;这里最后一个操作数 x是左值纯右值 decltype(x++)int内置后置自增表达式的结果是纯右值 纯右值 decltype(x + y)int当 x、y为int时,内置加法表达式的结果是纯右值纯右值 decltype(f())函数返回类型 当 f()的返回类型为非引用类型时,结果为该返回类型将亡值 decltype(std::move(x))int&&当 x是int类型的左值时,std::move(x)是类型为int&&的将亡值表达式将亡值 decltype(static_cast<int&&>(x))int&&将 x转换为int&&后,表达式是将亡值
- 注意,在下面的表格内容中:
推导案例
1 |
|
程序运行的结果如下:
1 | decltype(a) = int |
作用域函数名
推导规则
decltype作用于函数名的推导规则:- 对未加括号的函数名使用
decltype,返回函数的声明类型本身,而非函数指针,不会发生函数到函数指针的转换。 - 要点:
decltype(f)与decltype(&f)不同:前者得到函数类型int(),后者得到函数指针类型int(*)()。- 这与
auto的推导规则不同:auto x = f;中,函数名f会发生函数到函数指针的转换,推导结果为int(*)()。 - 函数名作为表达式时属于左值,但
decltype对未加括号的函数名采用特殊规则,直接返回函数的声明类型,不附加引用。 - 对函数名加上括号后,
decltype((f))按普通表达式规则处理。由于(f)是左值表达式,因此推导结果为函数左值引用类型int(&)()。 - 特别注意,
decltype只进行编译期类型分析,不会实际调用函数。
- 对未加括号的函数名使用
decltype作用于函数名的常见案例:函数声明 decltype使用示例推导结果 说明 int f();decltype(f)int()返回函数类型本身 int& f();decltype(f)int&()返回类型为 int&的函数类型void f(int);decltype(f)void(int)函数类型包含返回类型和参数类型 int f();decltype(&f)int(*)()取函数地址,得到函数指针类型 int f();decltype(f)*int(*)()在函数类型后添加指针,得到函数指针类型 int f();decltype(f)&int(&)()在函数类型后添加左值引用,得到函数左值引用类型 int f();decltype((f))int(&)()加括号后按左值表达式规则推导,得到函数左值引用类型
推导案例
1 |
|
程序运行的结果如下:
1 | decltype(a) = int () |
使用案例
使用案例一
decltype应付模板编程中可变类型
代码写法一,不使用
decltype,编译成功
1 |
|
代码写法二,不使用
decltype,编译失败
1 |
|
代码写法三,使用
decltype,解决编译失败的问题
1 |
|
使用案例二
decltype通过变量表达式抽取变量类型
1 |
|
程序运行的结果如下:
1 | size = 3 |
使用案例三
C++ 版本说明
- C++ 11:支持
auto类型推导和decltype类型推导,但不能将二者组合成decltype(auto)来使用。 - C++ 14 及以后:支持
decltype(auto),可以根据初始化表达式或返回表达式,按照decltype的规则推导类型,并保留引用、const等类型属性。
decltype(auto)用于变量声明
1 |
|
decltype(auto)用于函数返回类型
1 |
|
使用案例四
什么是函数返回类型后置语法
函数返回类型后置语法(Trailing Return Type)是 C++ 11 引入的一种函数声明语法,使用 auto 声明函数,并通过 -> 将返回类型放在参数列表之后。该语法通常与 decltype 配合使用,可以根据参数表达式推导函数的返回类型,尤其适用于模板编程中返回类型依赖于参数类型的场景。使用案例如下:
1 | auto add(int i, int k) -> decltype(i + k) { |
代码分析
上述代码使用 auto 声明函数 add,通过 -> decltype(i + k) 将返回类型声明在参数列表之后。decltype(i + k) 用于推导表达式 i + k 的类型,由于 i 和 k 均为 int 类型,因此函数的返回类型为 int。上述代码等价于传统的 int add(int i, int k) 写法。
auto结合decltype构成函数返回类型后置语法
1 |
|
程序运行的结果如下:
1 | int &tf(int &i) |
代码分析
- 演示了函数返回类型后置语法结合
decltype,根据模板参数及函数重载结果推导函数返回类型的用法。 Func()使用函数返回类型后置语法,通过decltype(tf(t))根据tf(t)表达式推导返回类型。- 当传入
int类型的左值时,匹配int& tf(int& i),因此Func()的返回类型为int&。 - 当传入
double类型的左值时,匹配double tf(double& d),因此Func()的返回类型为double。 - 注意,这里的
auto并没有类型推导的语义,它只是函数返回类型后置语法的组成部分。
