C++11开始引入了
decltype
关键字,和auto
一样,它也是用于获取和推断对象和表达式类型的。它们两者在类型推断上,有什么不同呢?
左值、右值、将亡值
因为 auto 和 decltype 的类型推导规则涉及到对 表达式值类别的区分,这里先简单介绍C++11以后的左值、右值和将亡值概念。 如果更具体的细分,除了上面三种值类别,还有泛左值和泛右值,这两个是在纯左值和右值的基础上,加上将亡值。
- 左值(lvalue):具名、可取地址且不可被移动。
- 右值(rvalue):不具名、不可取地址,可以被移动。
- 将亡值(xvalue):具名、可被移动。
左值
特征
- 可通过取地址运算符获取其地址
- 可修改的左值可用作内建赋值和内建符合赋值运算符的左操作数
- 可以用来初始化左值引用(后面有讲)
左值类型
- 变量名、函数名以及数据成员名
- 返回左值引用的函数调用
- 由赋值运算符或复合赋值运算符连接的表达式,如(
a=b, a-=b
等) - 解引用表达式
*ptr
- 前置自增和自减表达式(
++a, ++b
) - 成员访问(点)运算符的结果
- 由指针访问成员( -> )运算符的结果
- 下标运算符的结果(
[]
) - 字符串字面值(
"abc"
)
纯右值
在前面有提过,自C++11开始,纯右值(pvalue, pure ravlue)相当于之前的右值,那么什么是纯右值呢?
右值类型
- 字面值或者函数返回的非引用都是纯右值。
- 以下表达式的值都是纯右值:
- 字面值(字符串字面值除外),例如
1,'a', true
等 - 返回值为非引用的函数调用或操作符重载,如:
str.substr(1, 2), str1 + str2, or it++
- 后置自增和自减表达式(
a++, a--
) - 算术表达式
- 逻辑表达式
- 比较表达式
- 取地址表达式
- lambda表达式
将亡值
将亡值(xvalue, eXpiring value),顾名思义即将消亡的值,是C++11新增的跟右值引用相关的表达式,通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。
xvalue 只能通过两种方式来获得,这两种方式都涉及到将一个左值赋给(转化为)一个右值引用:
- 返回右值引用的函数的调用表达式,如
static_castt<T&&>(obj)
; 该表达式得到一个 xvalue - 转换为右值引用的转换函数的调用表达式,如:
std::move(t)、satic_cast<T&&>(obj)
表达式
C/C++代码是由标识符、表达式和语句以及一些必要的符号(大括号等)组成。
表达式由按照语言规则排列的运算符,常量和变量组成。一个表达式可以包含一个或多个操作数,零个或多个运算符来计算值。单一变量是最简单的一种表达式。
auto
C++11 重新定义了auto的含义以后,在之后发布的C++14、C++17等标准对auto的更新、增强的功能
auto
的 "自动推导" 能力只能用在 "初始化" 的场合。包括 赋值初始化 或者 花括号初始化 ( Initializer list ),变量右边必须是一个表达式。如果纯是一个变量声明,则无法使用 auto 。
auto 的推导规则有些复杂,跟函数模板参数的自动推导有相似之处( 具体函数模板参数自动推导规则可以参考《Effective Modern C++》条例2)。
推导规则
使用auto进行变量类型自动推导的格式一般如下:
cpp
[const,volatile] auto [*, &, &&] var = expr; // []内的内容表示可选
const 与 volatile修饰词,这两个称为CV修饰词或者CV限定符。
按照上面的形式,根据 "=" 左边auto的修饰情况分为三种情形:
- 规则一 :只有auto的情况,非引用和指针,表示按值初始化 ,CV限定符和引用将被忽略
cpp
int a = 1;
int& b = a;
const int& c = a;
// 忽略=号右边的引用,auto被推导为int,d是int类型
auto d = b;
// 忽略=号右边的const属性和引用,auto被推导为int,e是int类型
auto e = c;
- 规则二 :形式如 auto& 或者 auto*,表示定义引用和指针,CV限定符将被保留。
cpp
int x = 1;
const int cx = x;
const int& crx = x;
auto& i = x; // (1) i为 int&
auto& ci = cx; // (2) ci为 const int&
auto* pi = &crx; // (3) pi为 const int*
除了下面的第3种情况外 (万能引用),auto都不会推导出结果是引用的类型,如果要定义为引用类型,就要像上面那样明确地写出来。
但是auto可以推导出来是指针类型 ,也就是说就算没有明确写出 auto*
,如果 expr 的类型是指针类型的话,auto则会被推导为指针类型,这时expr的const属性也会得到保留,如下的例子:
cpp
int i = 1;
auto pi = &i; // pi为int*
const char word[] = "Hello world!";
auto str = word; // str为const char*
pi
被推导出来的类型为 int*,而 str 被推导出来的类型为 const char*。
- 规则三:形式如 auto&&, 表示万能引用。
这种情况下,其推导规则与模板类型的推导规则很相似,参考 《Effective Modern C++》条例1。
当以 auto&&
的形式出现时,它表示的是万能引用而非右值引用,这时将视 expr 的类型分为两种情况:
- 如果 expr 是个左值,那么它推导出来的结果是一个左值引用,这是auto被推导为引用类型的唯一情形。
- 如果 expr 是个右值,那么将依据上面的第一种情形的规则。 如下的例子:
cpp
int x = 1;
const int cx = x;
auto&& ref1 = x; // (1) ref1为int&
auto&& ref2 = cx; // (2) ref2为const int&
auto&& ref3 = 2; // (3) ref3为int&&
decltype
decltype 与 auto 关键字一样,用于进行 编译时类型推导,不过它与 auto 还是有一些区别的:
auto 通过初始化它的表达式来推断变量的类型,也即 auto 推导变量依赖于初始化它的表达式,并且auto声明的变量必须初始;
decltype 的类型推导是 以一个普通表达式作为参数,返回该表达式的类型, 而且 decltype 并不会对表达式进行求值。
decltype 进行类型推导的形式一般如下:
cpp
decltype(expr) v [ = another_expr]; // []内的内容表示可选
除了C++14开始支持的 decltype(auto)这种形式,其他情况下 decltye(expr) 可以与CV限定符以及&、 等联用*,比如:
cpp
int x = 100;
decltype(x)* px = &x;
const decltype(x)& crx = x;
推导规则
使用 decltype(expr)
获取类型时,编译器将根据以下规则得出结果:
- 如果 expr 是一个单独变量**(结构化绑定除外)或者类成员访问**。
- 变量带有括号,则
decltype(expr)
返回变量定义时的类型(保持 const 和引用属性) - 变量不带括号,则
decltype(expr)
返回T&
。
- 变量带有括号,则
- 如果 expr 是一个 函数或者仿函数 ==调用== ,
decltype(expr)
返回函数返回值的类型。 - 如果 expr 是其他类型为 T 的表达式,且:
- 若 expr 的值类别为 左值(lvalue),则
decltype
产生T&
; - 若 expr 的值类别为 右值(rvalue),则
decltype
产生T
; - 若 expr 的值类别为 将亡值(xvalue), 则
decltype
产生T&&
。
- 若 expr 的值类别为 左值(lvalue),则
[!NOTE]
对于 expr 是 单纯变量的情况,
decltype(x)
和decltype((x))
通常是不同的类型。
cpp
struct Foo { int a; };
using Func = Foo& ();
using Array = char[2];
const Foo f_v();
Foo& f_ref();
Foo&& f_r();
int minus(int x, int y) {
return x - y;
}
int a = 0;
const int b = 1;
const Foo foo = {10};
Foo&& rref = Foo{1};
const Foo& ref = foo;
char c[2] = {1, 2};
int* p = &a;
const Foo* pFoo = &foo;
// 1 单个变量
// 1.1 不带括号的单个变量
decltype(a) v1; // int
decltype(b) v2; // const int
decltype(foo) v3; // const Foo
decltype(ref) v4; // const Foo&
decltype(rref) v5; // Foo&&
decltype(c) v6; // char[2]
decltype(p) v7; // int*
decltype(foo.a) v8; // int
// 1.2 带括号的单个变量
decltype((a)) v1_2; // int&
decltype((foo)) v2_2; // const Foo&
decltype((foo.a)) v3_2; // const int&
// 2 函数调用
decltype(f_v()) v10; // const Foo
// 3.1 左值
decltype(a += 10) v11; // int&
decltype(++a)) v12; // int&
decltype(c[1]) v13; // char&
decltype(*p) v14; // int&
// 推导函数类型
decltype(minus) v15_1; // int(int,int)
decltype((minus)) v15_2; // int(&)(int, int)
decltype(minus)* v15_2; // int(*)(int, int)
// 3.2 右值
decltype(a++) v16; // int
decltype(&b) v17; // const int*
decltype(1+2) v18; // int
// 3.3 将亡值
decltype(std::move(a)) v19; // int&&
当 decltype(expr) 传入的是 函数名时,需要注意⚠️:虽然 C/C++ 多数时候会 隐式の把函数名转换成对应的函数指针,但这两者本身并不等价。
decltype(func_name)
推导出来的并不是 函数指针,而是函数类型!比如上面代码中的
decltype(minus)
, 得到的时候 函数 minus 的类型:int(int, int)
,而其对应的函数指针形式应该是int(*)(int, int)
;如果想得到函数指针,需要显式的加上 '*' :
decltype(minus)* func_ptr;
。
其他用法
decltype(auto)
C++14 运行时用 decltype(auto) 告诉编译器使用 decltype 的规则来推导 auto.
decltype(auto)
必须单独使用,也就是不能再给它加上指针、引用还有cv
属性。
cpp
template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y) -> decltype(_Tx*_Ty) {
return x * y;
}
与using、typedef结合使用
cpp
using size_t = decltype(sizeof(0));
using nullptr_t = decltype(nullptr);
vector<int> vec;
typedef decltype(vec.begin()) vectype;
for(vectype i = vec.begin(); i != vec.end(); i++) {
//...
}