C++ 值类别、auto与decltype

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) 获取类型时,编译器将根据以下规则得出结果:

  1. 如果 expr 是一个单独变量**(结构化绑定除外)或者类成员访问**。
    1. 变量带有括号,则 decltype(expr) 返回变量定义时的类型(保持 const 和引用属性)
    2. 变量不带括号,则 decltype(expr) 返回 T&
  2. 如果 expr 是一个 函数或者仿函数 ==调用==decltype(expr)返回函数返回值的类型。
  3. 如果 expr 是其他类型为 T 的表达式,且:
    1. 若 expr 的值类别为 左值(lvalue),则 decltype 产生 T&
    2. 若 expr 的值类别为 右值(rvalue),则 decltype 产生 T
    3. 若 expr 的值类别为 将亡值(xvalue), 则 decltype 产生 T&&

[!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++) {
   //...
}

参考

相关推荐
Xua305516 分钟前
浅谈Spring Cloud:OpenFeign
后端·spring·spring cloud
fat house cat_36 分钟前
volatile,原来是这么回事
java·jvm·面试·volatile
蓝染-惣右介1 小时前
【若依RuoYi-Vue | 项目实战】帝可得后台管理系统(二)
java·前端·后端·vue·springboot
齐 飞1 小时前
Java接口和抽象类的区别
java·笔记·后端
软件测试曦曦1 小时前
外包干了4年,技术退步太明显了。。。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
打鱼又晒网1 小时前
你了解system V的ipc底层如何设计的吗?消息队列互相通信的原理是什么呢?是否经常将信号量和信号混淆呢?——问题详解
linux·运维·服务器·后端·操作系统
鱼跃鹰飞1 小时前
Leetcode面试经典150题-198.打家劫舍
数据结构·算法·leetcode·面试·职场和发展
(⊙o⊙)~哦2 小时前
spring boot 定时器配置
java·spring boot·后端
C—328G2 小时前
flask
后端·python·flask
科爷出击2 小时前
ElasticSearch学习笔记
大数据·后端