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

参考

相关推荐
PAK向日葵23 分钟前
【算法导论】PDD 0817笔试题题解
算法·面试
uzong2 小时前
技术故障复盘模版
后端
GetcharZp2 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程3 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研3 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy4 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack5 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9655 小时前
pip install 已经不再安全
后端