What & How & Why

表达式

第 4 章笔记


表达式基础

  • 由操作数组成
  • 可求值,并且(通常)返回结果(求值广义上是对系统产生影响的操作)
  • 最基本的表达式:
    • 变量 / 常量(字面值)
  • 通常表达式会包含运算符,可复合

操作符

  • 表达式的重要组成部分
操作符的特性
  • 可以接收多少操作数:元数
  • 可以接收什么类型的操作数:不满足类型要求时,可能存在类型转换
  • 操作数是左值还是右值
  • 结果的类型(考虑结果主要是在复合的情况下)
  • 结果是左值还是右值
  • 操作符的优先级和结合性:
    • 参考 C++ Operator precedene
    • 括号可以改变优先级
    • 相同优先级具有相同的结合性
  • 操作符重载:不改变操作数的个数,优先级和结合性
操作符求值顺序的不确定性

int x = 0;
// C++ 没有规定此类的表达式的顺序
// complier depended
func(x = x  + 1; x = x + 1) ;

左值与右值

  • 左右之分是基于表达式来判定的
glvalue (generalized) 泛左值
  • 表达式
  • 求值结果可以确定一个对象 / 内存位置 / 函数
  • 起到了一个标识的作用

// 对 x 的评估结果是获得 x 关联的内存位置
x = 3;

prvalue(pure) 纯右值
  • 只能作为操作数来使用(不能作为赋值的对象)
  • 可以被用于初始化某个对象
  • 构造临时对象时,也是右值

x = 3; // used for init
3 + 2; // used as operands

// temp objects is pvalue
int{};

xvalue 亡值
  • 资源可以被重新使用的值

std::vector<int> x;
// 调用右值引用
void fun(std::vector<int>&& par)
// x 被转变为了亡值, x 对应的 vector 资源已经被转交
fun(std::move(x));

左值右值的判断
  • 左值:
    • gvalue:就是标识了一个一个对象,位或者函数
    • 不是 xvalue:并不是即将消除的值
  • 右值:
    • 是一个即将消亡的值
    • 如果用于初始化或者 operand,则是纯右值 prvalue
  • 与 C 不同,左值和右值与 = 的左右没有特定的关系

// x 是左值
const int x = 3;
// error, x 不能放到 = 左边
// x 是 immutiable value
x = 3;

// 右值可以放到 = 左边
struct Str {};
int main()
{
// Str() 是临时变量,是右值
    Str() = str();
}

左值与右值的转换

//x, y 是左值
int x = 3;
int y = 3;
// + 需要右值作为 operand,此时传入的是左值
// 左值在这里就转变为了右值 
// l to r conversion
x + y

Temporary materialization

struct Str
{
    int x;
}

int main()
{
    // prvalue temp object
    Str();
    // 从 Str() 临时对象中取出 x 代表的数据
    // 这个过程中,取值过程将 Str() 代表的临时对象转变了有标识的值
    // 所以 Str() 标识了一个位置,但又即将消亡
    // 也就是 prvalue 到 gvalue 转变
    Str().x;
}
void fun(const int& par) {}
int main()
{
    //3 从 rvalue 转换为 xvalue
    fun(3);
}

decltype 与左右值
  • 接收表达式
    • xvalue 返回 T&&
    • lvalue 返回 T&
    • prvalue 返回 T

int main()
{
    int x;
    // y 是 int 类型,x 是实体
    decltype(x) y;
    // z 是 int& 类型, (x) 是表达式
    decltype((x)) z;
    // w 是 int&&
    decltype(std::move(x)) w = std::move(x);
}

Entity: unparenthesized id-expression or an unparenthesized class member access expression.

类型转换

隐式转换
  • 隐式转换由下列的有限转换序列组成:
    • 0 或 1 个标准转换序列(standard conversion
    • 0 或 1 个用户定义的转换序列(user-defined conversion)
  • 一个标准转换序列包括:
    1. 左值到右值的转换,数组到指针的转换,以及函数到指针的转换
    2. 数值的提升与转换(numeric promotion & conversion

Reference: Implicit conversions

  • 数值提升(无损):
    • 整型提升
    • 浮点型提升
  • 数值转换(可能有损):
    • 可能会更改值,因为两种类型的数据可能存在对方不能表示的范围

// 数值提升:低精度自动到高精度
// 整型提升:e.g. int to float
3 + 0.5;
// 浮点型提升:e.g. float to double

// 数值转换
// 不是所有的类型都能进行隐式转换
// error, 字符串不能转换为 double
"abc" + 0.5;

显式转换

显式转换的意义:处理隐式转换无法处理的场景:

int x = 3;
int y = 4;
// 使用显式转换得到浮点数结果
// x 被隐式转换(左值->右值->double),y 被显式转换
std::cout << (x / static_cast<double>(y));

  • static_cast<>():() 中是被转换的值,<> 中是需要被转换的类型

// 显式的将数值从 int 转化为 double
static+cast<double>(3) + 0.5;

类型转换有局限性,不是所有类型都可以互转,无论是显性还是隐性

  • 可以将基类的引用转换为派生类的引用
  • 可以将 void 类型的指针转换为任意类型的指针(这种转换不被隐式转换支持)
  • 编译器完成
  • 无法去除常量性(使用 const_cast)
  • const_cast<>():转换当前表达式的 const

int x = 3;
const int& ref = x;
// ref2 是 non-const 的引用
int& ref2 = const_cast<int&>(ref);

注意:如果是绑定常量的引用,请不要使用 const_cast 改变其常量性。编译器对常量经常会进行编译优化,这往往是基于编译器来进行的。这种行为是 undefined 的,非常危险。

  • reinterpret_cast<>():将当前类型重新解释为另外一种类型(将当前内存空间以另外一种形式来解释)
    • 主要用于将指针类型与其对应的类型相互转换

int x = 3;
int *ptr = &x;
// 强行解释 int 到 double, 通过指针
// 指针转换后,会以 double 的方式解引用当前的 int
// 由于 double 需要 8 位, int 4 位,则解引用会将后 4 位的内存内容与前四位合并,并解引用
double * ptr2 = reinterpret_cast<double>(ptr);

  • C-style 转换

c-style 转换会以特定的执行顺序执行 C++ cast 来进行转换。该转换过程是由编译器通过尝试得出的,并不完全可控。最好的方式是避免在 C++ 中使用诸如此类风格的转换。

// 不推荐在 C++ 中使用
int x = 3;
(double)x;

C++ 希望用户尽量少使用显性的类型转换(名字又臭又长)。

表达式详述

算术运算符

  • 分为三个优先级
    • 正(+),负(-)(一元)
    • 乘除 / 末除(二元)
    • 加减(二元)
  • 左结合
  • 通常 operand 和 结果都是右值

// rvalue
3 + 5
// lvalue->rvalue->rvalue
int x = 3;
int y = 5;
x + y

  • 加减法可以引用于进行指针移位的运算
  • 正负操作符会进行整型提升

// y 是 int 类型
short x = 3;
auto y = +x;

  • 整数的除法会向 0取整

// 1
4 / 3
// - 1
-4 / 3

  • 求余:只能接收整数 operand
  • m%n 时,mm%n 的结果同号

逻辑与关系操作符

  • 关系操作符:判断大小相等
    • 不能串联多个多个关系运算符(比如 a>b>c
    • 比较时,会将 bool 转换为其他类型,因此不要写 if(a == true) 的写法。
    • 三路比较 (C++ 20)
      • 可以直接先求出 a 和 b 的关系,然后使用该关系的结果用于 if-else,效率高
      • 返回值类型:
        • std::strong_ordering:包含了一些关系相关的常量(比如 std::strong_odering::less,包含相等和等价)
        • std::weak_ordering:不包含相等关系(用于只能说等价,不能说相等的数据结构,)
        • std::partial_ordering:还包含了一种 unordered 的关系。
          • 比如两个浮点数比较时,无法区分正负的 0,因此是一种等价但无法区别的关系
          • 另外一个例子是 NaN(not a number),任意数与其比较都会返回 partial ordering 的关系。
  • 逻辑操作符:与或非
    • 只要 operand 可以转换为 bool 即可运算
    • operand 和 结果都是右值
    • 除了逻辑非,其他的结合方式都是左结合
    • 具有短路逻辑(short-circual):左边为真后右边不会执行
    • 优先级:与高于或

位操作符

  • ~: 按位取反,|:按位或,&:按位与,^ 按位异或
  • 接受右值,返回右值
  • 除取反,都是左结合
  • 计算过程中可能会存在整型提升
  • 不存在短路逻辑

char x = 3; // 00000011
~x; // -4, 11111100
char y = 5; // 00000101
x & y; // 00000001
x | y; // 000000111
x ^ y; // 00000110

左移右移
  • 缺出来的位置用 0 补全
  • 一定条件下可以替代乘/除 2,并且速度更快

char x = 3; //00000011
x >> 1; // 00000001
char y = -4; // 11111100
// 与输出操作符合并使用时,需要使用括号进行重载
std::cout << (y << 1); // 11111000

整型提升会根据符号位来填充

unsigned char x = 3;
// char 到 int 的提升
// unsigned 会按位进行 0 的补全
unsigned char z = 0xff // 11111111
// 0000...00011111111 总共32位
auto y = ~x; // 结果为 256

//signed 的提升会按照符号位来进行补全的提升,这里是 1
signed char z = 3;
// 提升过后的值为 11111......111111
// 求反后的结果是 00000....000000
y = ~z; // 结果是 0

赋值操作符

  • 赋值操作符左边为可修改的左值,右边为可转换为左边类型的右值。
  • 赋值操作符为右结合(先评估等号右边)
  • 求值结果为左算子
  • 可以引入大括号防止 narrowing converstion

short x;
// error, can't store a unsigned int to short
x = {0x80000003};
// 无精度损失的转换不会被阻止
x = {3};
// 只要存在 norrowing conversion 的可能,编译器就不会通过
// y 可能会被修改导致 norrowing conversion
int y = 3;
x = {y};
// 使用编译器期 const 确保 y 不会被修改
constexpr int y = 3;
x = {y};

  • 赋值操作符的优先级非常低
交换两个数

// bitwise xor
int x = 2;
int y = 3;
// x = 2^3 | y = 3
x^=y;
// 任何数与 0 xor 结果都是其本身
// x = 2^3 | y = 3^2^3 = 2^3^3 = 2^0 = 2
y^=x;
// x =2^3^2 = 3 | y = 2
x^=y; // 最后结果 x = 3, y =2

自增 / 自减操作符

  • 后缀 i++:返回 i (的原始值),再自增
  • 前缀 ++i:自增,再返回 i (运算之后的)值
  • 前缀返回左值,后缀返回右值
  • 后缀在返回的时候当前变量已经更新,因此只能返回一个历史内容,属于临时变量,因此是右值
  • 前缀可以视作 x = x + 1; 得到是左值。该左值还可以接着放到等号左边,因此 ++(++x) 也是合法的。
  • 推荐使用前缀:后缀会创造临时变量并返回,效率较低。
  • 后缀一般用于需要利用返回值的时候,比如运算符重载时(类)可能会用到

其他操作符

成员访问操作符
  • . 成员访问操作符
  • 实质是 通过(this)指针访问成员

struct Str { int x };
int main()
{
    Str a;
    // a 是左值,返回左值
    a.x;
    // Str() 是 右值, 返回值为右值引用
    Str().x;
    //(*ptr).x,返回左值
    ptr->x;
}

三元条件操作符

// 只会求值一个分支
true ? 3:5;
// 条件表达式返回的类型必须相同
ture ? 1: "hello"; // error
// 都是左值,则返回左值,否则返回右值
int x = 0;
false ? 1 : x; // 返回右值
// 右结合
// 先判断 score == 0
int score = 100;
int res = (score > 0) ? 1: (score == 0) ? 0:-1;

逗号操作符
  • 典型应用:
    • for 循环中可以写出较为复杂的语句
    • 元编程:折叠表达式,包展开
  • 函数的参数表达式不是逗号操作符,参数列表求值顺序不定

// 确保操作数从左向右求值
// 求值结果为右算子
2, 3; // result is 3
// 左结合
// (2, 3) , 4
2, 3, 4;

sizeof
  • 返回类型 / 对象 / 表达式返回值占用的字节数

int x;
// 推荐统一使用带括号的形式
sizeof(int);
sizeof(x);
// 对表达式评估时,不会真正执行求值
int* ptr = nullptr;
// 等价 sizeof(int)
sizeof(*ptr);

域操作符

用于访问域内的变量

int = x;
namspace ABC
{
   int x;
}
int main()
{
   int x;
   int y = x; // local
   int y = ::x; // global
   int y = ABC::x // ABC
}

C++17表达式求值顺序

  • 之前的限定求值:逗号,三元条件,逻辑与 / 或(短路)
  • C++17 新引入的限定

// 先求 e1,再求 e2
e1[e2];
e1.e2;
e1.*e2;
e1->*e2;
e1<<e2;
e1>>e2;
e2=e1 / e2+=e1/ e2*=e1;

  • newType(e) 会先分配内存再求值