作用
decltype关键字是为了解决auto关键字只能对变量进行类型推导的缺陷而出现的。它的用法和typeof很相似:decltype(表达式)
。
推导表达式的值
有时候,我们需要计算某个表达式类型,比如:
auto x = 1.0;
auto y = 2;
decltype(x + y) z = 0;
printf("%f\n", z);
if(std::is_same<decltype(x), int>::value){
printf("%s", "type x == int\n");
}
if(std::is_same<decltype(x), double>::value){
printf("%s", "type x == double\n");
}
if(std::is_same<decltype(x), decltype(z)>::value){
printf("%s", "type x == type z\n");
}
cv限制符的继承与冗余的符号
与auto类型推导时不能“带走”cv限制符不同,decltype是能够“带走”表达式的cv限定符的。如果,如果对象的定义中有const或者volatile限制符,使用decltype进行类型推导时,其成员不会继承const或者volatile限制符。
#include <iostream>
#include <type_traits>
const int ic = 0;
volatile int iv;
struct S{
int i;
};
const S a = {0};
volatile S b;
volatile S* p = &b;
int main(){
printf("%d\n", std::is_const<decltype(ic)>::value); // 1
printf("%d\n", std::is_volatile<decltype(iv)>::value); // 1
printf("%d\n", std::is_const<decltype(a)>::value); // 1
printf("%d\n", std::is_volatile<decltype(b)>::value); // 1
printf("%d\n", std::is_const<decltype(a.i)>::value); // 0
printf("%d\n", std::is_volatile<decltype(p->i)>::value); // 0
}
而与auto相同的是,decltype从表达式推导出类型之后,进行类型定义时,也会允许一些冗余的符合。比如cv限制符以及引用符合&,通常情况下,如果推导出的类型已经有了这些属性,冗余的符合将会被忽略:
#include <iostream>
#include <type_traits>
int i = 1;
int &j = i;
int *p = &i;
const int k = i;
int main(){
decltype(i) & var1 = i; // --> 左值引用
decltype(j) & var2 = i; // 冗余的&,被忽略 -- 左值引用
printf("%d\t", std::is_lvalue_reference<decltype(var1)>::value);
printf("%d\n", std::is_lvalue_reference<decltype(var2)>::value);
//decltype(p)* var3 = &i; // 无法通过编译
decltype(p)* var3 = &p; // var3类型是 int **
auto *v3 = p; // v3 类型是 int *
v3 = &i;
const decltype(k) var4 = i; // 冗余的const,被忽略 (volatile规则和const一样)
}
特别要注意的是 decltype§*, decltype之后的*
并不会被忽略
类型拓展
在C++11头文件中,常常会看到这样的代码:
using size_t = decltype(sizeof(0));
using ptrdiff_t = decltype((int*)0 - (int*)0);
using nullptr_t = decltype(nullptr);
这里的size_t、ptrdiff_t 、nullptr_t 都是由decltype推导出的类型。这种定义方式非常有意思。在一些常量、基本类型、运算符、操作符等已经被定义好的情况下,类型可以按照规则被推导出。而使用using,就可以为这些类型取名。这就颠覆了之前类型拓展需要将扩展类型“映射”到基本类型的做法。
提高代码的可读性
#include <iostream>
#include <vector>
int main(){
std::vector<int> vec;
typedef decltype(vec.begin()) vectype;
for(vectype i = vec.begin(); i < vec.end(); i++){
}
for(decltype(vec)::iterator i = vec.begin(); i < vec.end(); i++){
}
}
尾返回类型推导
目的:推导函数的返回类型
下面我们看个例子,在传统C++中,可以这样写:
template<typename R, typename T, typename U>
R add(T x, U y){
return x + y;
}
- 这样写代码不好,因为程序员在使用这个模板的时候,必须明确指出返回类型。但是,事实上我们并不知道add()这个函数会做什么样的操作,会获取一个什么样的返回类型
- 注意:
typename
和class
在模板参数列表中并没有区别,在typename
这个关键字出现之前,都是使用class来定义模板参数的,但在模板中定义有嵌套依赖类型的变量时,需要用typename来消除歧义
C++11中引入了decltype
类型,这时你可能会这样写:
decltype(x + y)add(T x, U y);
- 但是实际上这样并不能通过编译:因为在编译器读到
decltype(x + y)
时,x和y尚未被定义。 - 为了解决这个问题,C++11引入了尾返回类型,利用
auto
关键字将返回类型后置:
template<typename T, typename U>
auto add(T x, U y)->decltype(x + y){
return x + y;
}
而C++14开始可以直接让普通函数具备返回值推导,也就是说,下面的写法是正确的:
template<typename T, typename U>
auto add(T x, U y){
return x + y;
}
完整的例子:
#include <iostream>
#include <type_traits>
// before c++11
template<typename R, typename T, typename U>
R add(T x, U y) {
return x + y;
}
// after c++11
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){
return x + y;
}
// after c++14
template<typename T, typename U>
auto add3(T x, U y){
return x + y;
}
int main() {
// before c++11
int z = add<int, int, int>(1, 2);
std::cout << z << std::endl;
// after c++11
auto w = add2<int, double>(1, 2.0);
if (std::is_same<decltype(w), double>::value) {
std::cout << "w is double: ";
}
std::cout << w << std::endl;
// after c++14
auto q = add3<double, int>(1.0, 2);
std::cout << "q: " << q << std::endl;
return 0;
}
© 2021 GitHub, I
decltype(auto)
decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。
template<int i>
struct Int {};
constexpr auto iter(Int<0>) -> Int<0>;
template<int i>
constexpr auto iter(Int<i>) {
return iter(Int<i-1>{});
}
int main() {
decltype(iter(Int<10>{})) a;
}
推导规则
当使用decltype(e)来获取类型时,编译器将依序判断下面四规则
- 如果e是一个没有带括号的标记符表达式(id-expression)或者类成员访问表达式,那么decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,则会导致编译时错误
- 否则,假设e的类型时T,如果e是一个将亡值,那么decltype(e)为T&&
- 否则,假设e的类型时T,如果e是一个左值,那么decltype(e)为T&
- 否则,假设e的类型时T,那么decltype(e)为T
标记符表达式:指的是除去关键字、字面量等编译器需要使用的标记之外的程序员自定义的标记都是标记符,而单个标记符对应的表达式就是标记符表达式。比如
int arr[4];
,那么arr是标记符表达式,而arr[3] + 0, arr[3]等,都不是标记符表达式。
那么,对于
int i;
decltype(i) a;
decltype((i)) b; //‘b’ declared as reference but not initialized
对于decltype(i) a;
,使用了规则1—因为i是一个标记符表达式,所以类型被推导为int,而decltype((i)) b;
中,由于(i)不是一个标记符表达式,而是左值表达式(可以有具名的地址),按照规则3,被推导为一个int的引用。
int i = 4;
int arr[5] = {0};
int *ptr = arr;
struct S{double d;}s;
void Overloaded(int)
void Overloaded(char); //重载的函数
int&& RvalRef();
const bool Func(int);
//规则1:单机标记符以及访问类成员,推到为本类型
decltype(arr) val1; //int[5]
decltype(ptr) val2; // int*
decltype(s.d) val3; // double;
decltype(Overloaded) value5; //无法通过编译,是个重载的函数
// 规则2:将亡值,推到为类型的引用
decltype(RvalRef()) var6 = 1; // int&&
// 规则3:左值,推导为类型的引用
decltype(true? i : i) val7 = i; // int&, 三元运算符,这里返回一个i的左值
decltype((i)) val8 = i; //int &, 带圆括号的左值
decltype(++i) var9 = i; // int &, ++i返回i的左值
decltype(arr[3]) var10 = i; //int&, []操作返回左值
decltype(*ptr) val11 = i; // int&, *操作符返回左值
decltype("lavl") varl2 = "lval"; //const char(&)[9], 字符串字面常量通常为左值
// 规则4:上面都不是,推导为本类型
decltype(1) var13 ; // int, 除了字符串字面量为右值
decltype(i++) var14; // int, i++返回右值
decltype(Func(1))varl16; // const bool, 可以忽略()
看起来很复杂,实际上我们只需要注意一下规则3带来的左值引用的推导(如果参数不是标志表达式或者类成员表达式,而且参数都为左值,推导出的类型均为左值引用)。一个简单的能够让编译器提示的方法是:如果使用decltype定义变量,那么先声明这个变量,再在其他语句中对其进行初始化。这样由于左值引用总是需要初始化的,编译器会报错提示。另外,C++11标准库中添加的模板类is_lvalue_reference(是不是左值引用),可以帮助程序员进行一些推导类型的识别,比如:
#include <iostream>
#include <vector>
int &&RvalRef();
int main(){
printf("%d\n", std::is_rvalue_reference<decltype(RvalRef())>::value);
}
另外,也有is_rvalue_reference确定是不是右值引用