优秀的编程知识分享平台

网站首页 > 技术文章 正文

c++ 表达式 (3)(c++基本表达式用法)

nanyue 2024-09-02 19:03:49 技术文章 9 ℃

c++ 表达式(1)

c++ 表达式 (2)

c++ 编译阶段


其他

  • 常量表达式能在编译期求值并在编译期语境(如模板实参、数组大小等等)中使用
https://zh.cppreference.com/w/cpp/language/constant_expression
定义能在编译时求值的表达式。
这种表达式能用作非类型模板实参、数组大小,并用于其他要求常量表达式的语境,例如
int n = 1;
std::array<int, n> a1;  // 错误:n 不是常量表达式
const int cn = 2;
std::array<int, cn> a2; // OK:cn 是常量表达式

核心常量表达式
核心常量表达式??是求值过程中不会对下列任一者求值的表达式:

1、this 指针,除了在 constexpr 函数之中作为该表达式的一部分求值的情况
2、经过静态或线程存储期变量声明,并且不可用于常量表达式的控制流
2、调用未声明为 constexpr 的函数(或构造函数)的函数调用表达式
constexpr int n = std::numeric_limits<int>::max(); // OK:max() 是 constexpr
constexpr int m = std::time(NULL); // 错误:std::time() 非 constexpr
4、调用已声明但未定义的 constexpr 函数
5、调用实例化无法满足constexpr 函数/构造函数要求的 constexpr 函数/构造函数模板
6、调用 constexpr 虚函数,除了所用的对象可用于常量表达式或它的生存期始于此表达式内的情况
7、会超出实现定义的极限的表达式
8、导致任何形式的核心语言未定义行为(包含有符号整数溢出、除以零、数组边界外的指针算术等)的操作
constexpr double d1 = 2.0 / 1.0; // OK
constexpr double d2 = 2.0 / 0.0; // 错误:未定义
constexpr int n = std::numeric_limits<int>::max() + 1; // 错误:溢出
int x, y, z[30];
constexpr auto e1 = &y - &x;        // 错误:未定义
constexpr auto e2 = &z[20] - &z[3]; // OK
constexpr std::bitset<2> a; 
constexpr bool b = a[2]; // 行为未定义,但未指定是否检测
9、(C++17 前) lambda 表达式
10、左值到右值隐式转换,除非应用于以下非 volatile 字面类型的泛左值
指代可用于常量表达式的对象,
int main()
{
    const std::size_t tabsize = 50;
    int tab[tabsize]; // OK:tabsize 是常量表达式
                      // 因为 tabsize 可用于常量表达式
                      // 因为它有 const 限定的整数类型,且它的初始化器是常量初始化器
 
    std::size_t n = 50;
    const std::size_t sz = n;
    int tab2[sz]; // 错误:sz 不是常量表达式
                  // 因为 sz 不可用于常量表达式
                  // 因为它的初始化器不是常量初始化器
}
指代生命期始于此表达式的求值之内的非 volatile 对象
11、对联合体的不活跃成员或它们的子对象实施的左值到右值隐式转换或修改操作(即使它与活跃成员共享公共起始序列也是如此)
12、(C++20 起) 应用到拥有不确定值的对象的左值到右值隐式转换
13、对活跃成员(如果存在)是 mutable 的联合体调用隐式定义的复制/移动构造函数或复制/移动赋值运算符,除非该联合体对象的生存期始于此表达式的求值之内
14、(C++20 前) 会更改联合体的活跃成员的赋值表达式
15、指代引用类型的变量或数据成员的 标识表达式,除非该引用可用于常量表达式,或它的生存期始于此表达式的求值之内
16、从 void 指针转换到对象指针类型 T*,除非这个指针指向一个与类型 T 相似的对象 (C++26 起)
17、(C++20 前) dynamic_cast
18、reinterpret_cast
19、(C++20 前) 伪析构函数调用
20、(C++14 前) 自增或自减运算符
21、(C++14 起) 修改对象,除非该对象拥有非 volatile 字面类型,且它的生存期始于此表达式的求值之内
constexpr int incr(int& n)
{
    return ++n;
}
 
constexpr int g(int k)
{
    constexpr int x = incr(k); // 错误:incr(k) 不是核心常量表达式
                               // 因为 k 的生存期在表达式 incr(k) 之外开始
    return x;
}
 
constexpr int h(int k)
{
    int x = incr(k); // OK:不要求 x 以核心常量表达式初始化
    return x;
}
 
constexpr int y = h(1); // OK:以值 2 初始化 y
                        // h(1) 是核心常量表达式
                        // 因为 k 的生存期始在表达式 h(1) 之内开始
22、(C++20 起) 对于生存期不在此表达式的求值内开始的对象的析构函数调用或伪析构函数调用
23、(C++20 前) 应用到多态类型泛左值的 typeid 表达式
24、new 表达式或对 std::allocator<T>::allocate 的调用,除非选中的分配函数是全局可替换分配函数,且它分配的存储会在此表达式的求值内解分配 (C++20 起)
25、delete 表达式或对 std::allocator<T>::deallocate 的调用,除非它解分配的是此表达式的求值内分配的存储区域 (C++20 起)
26、(C++20 起) 协程:await 表达式或 yield 表达式
27、(C++20 起) 结果未指定的三路比较运算符
28、结果未指定的相等或关系运算符
29、(C++14 前) 赋值或复合赋值运算符
30、throw 表达式
31、汇编声明
32、宏 va_arg 的调用
33、goto 语句
34、会抛出异常的 dynamic_cast 或 typeid 表达式
35、lambda 表达式中,提及 this 或提及于该 lambda 之外定义的变量,如果它是一次 ODR 使用
void g()
{
    const int n = 0;
 
    constexpr int j = *&n; // OK:lambda 表达式之外
 
    [=]
    {
        constexpr int i=n;  // OK:'n' 未被 ODR 使用且未在此处被俘获。
        constexpr int j=*&n;// 非良构:'&n' ODR 使用了 'n'。
    };
}
注意,如果 ODR 使用在对闭包的函数调用中发生,那么它不涉指 this 或外围变量,因为它所访问的是该闭包的数据成员

// OK:'v' 与 'm' 被 ODR 使用,但没有在嵌套于 lambda 内的常量表达式中出现
auto monad = [](auto v){ return [=]{ return v; }; };
auto bind = [](auto m){ return [=](auto fvm){ return fvm(m()); }; };
 
// 在常量表达式求值中,创建对自动对象的俘获是 OK 的。
static_assert(bind(monad(2))(monad)() == monad(2)());
(C++17 起)
即使表达式 E 不会求值以上任何一项,但是如果它会求值以下任何一项,那么未指定 E 是否还是核心常量表达式:

在标准库中具有未定义行为的操作。
宏 va_start 的调用。
对声明有 [[noreturn]] 属性的函数进行的调用,而该调用会返回调用方。
(C++11 起)
一条假设 [[assume(expr)]];,其中如果 expr 在该假设出现处求值,那么结果不是 true,并且 E 也不会因此(即在对 expr 的虚设求值中会对前一列表中任何一项求值)而变得不是核心常量表达式。
(C++23 起)
本节未完成
原因:需要更多小示例和更少标准用语
注意:核心常量表达式本身并无任何直接的语义含义:表达式必须是常量表达式的子集之一(见后述),才可以在特定语境中使用。
  • sizeof
https://zh.cppreference.com/w/cpp/language/sizeof
查询对象或类型的大小。
在需要知道对象的实际大小时使用。
语法
sizeof( 类型 )	(1)	
sizeof 表达式	(2)	
两个版本都是 std::size_t 类型的常量表达式。

类型	-	类型标识(见类型的命名)
表达式	-	运算符优先级不低于 sizeof 的表达式(例如 sizeof a + b 会解析成 (sizeof a) + b 而不是 sizeof (a + b))
解释
1) 产生类型 的对象表示的字节数。
2) 产生表达式 的类型的对象表示的字节数,假如该表达式被求值。
注解
取决于计算机架构,字节可能具有 8 或更多位,确切的位数记录于 CHAR_BIT。

下列 sizeof 表达式始终求值为 1:

sizeof(char)
sizeof(signed char)
sizeof(unsigned char)
sizeof(std::byte)
(C++17 起)
sizeof(char8_t)
(C++20 起)
    
不能对函数类型、不完整类型或位域左值 (C++11 前)泛左值 (C++11 起)使用 sizeof。
当应用于引用类型时,结果是被引用类型的大小。
当应用于类类型时,结果是该类的完整对象所占据的字节数,包括这种对象放入数组时所需的任何额外填充。潜在重叠的子对象所占据的字节数可以小于该对象的大小。
sizeof 的结果始终非零,即使应用于空类。
当应用于某个表达式时,sizeof 并不对表达式进行求值(即该表达式是不求值操作数) (C++11 起),并且即便表达式代表多态对象,它的结果也是该表达式的静态类型的大小。不进行左值向右值、数组向指针和函数向指针转换。不过,它在形式上对纯右值实参进行临时量实质化:实参不可析构时程序非良构。 (C++17 起)    
    
#include <cstdlib>
#include <iostream>
 
struct Empty          { };
struct Base           { int a; };
struct Derived : Base { int b; };
struct Bit            { unsigned bit: 1; };
struct CharChar       { char c; char c2; };
struct CharCharInt    { char c; char c2; int i; };
struct IntCharChar    { int i;  char c;  char c2; };
struct CharIntChar    { char c; int i;   char c2; };
struct CharShortChar  { char c; short s; char c2; };
 
int main()
{
    Empty e;
    Derived d;
    Base& b = d;
    [[maybe_unused]] Bit bit;
    int a[10];
 
    auto f = [&]() { return sizeof(int[10]) == sizeof a ? throw 1 : e; };
//  f(); // 返回类型是 Empty,但是必然会抛出 1
 
    auto println = [](auto rem, std::size_t size) { std::cout << rem << size << '\n'; };
 
    println( "1) 空类的大小:                        ", sizeof e                     );
    println( "2) 指针的大小:                        ", sizeof &e                    );
    println( "3) sizeof(Bit)    -类:               ", sizeof(Bit)                  );
    println( "4) sizeof(int[10])-含有10个 int 的数组:", sizeof(int[10])              );
    println( "5) sizeof a       -含有10个 int 的数组:", sizeof a                     );
    println( "6) 含有10个 int 的数组的长度:           ", ((sizeof a) / (sizeof *a))   );
    println( "7) 含有10个 int 的数组的长度 (2):       ", ((sizeof a) / (sizeof a[0])) );
    println( "8) Derived 的大小:                    ", sizeof d                     );
    println( "9) 通过 Base 获取 Derived 的大小:      ", sizeof b                     );
    println( "A) sizeof(unsigned):                 ", sizeof(unsigned)             );
    println( "B) sizeof(int):                      ", sizeof(int)                  );
    println( "C) sizeof(short):                    ", sizeof(short)                );
    println( "D) sizeof(char):                     ", sizeof(char)                 );
    println( "E) sizeof(CharChar):                 ", sizeof(CharChar)             );
    println( "F) sizeof(CharCharInt):              ", sizeof(CharCharInt)          );
    println( "G) sizeof(IntCharChar):              ", sizeof(IntCharChar)          );
    println( "H) sizeof(CharIntChar):              ", sizeof(CharIntChar)          );
    println( "I) sizeof(CharShortChar):            ", sizeof(CharShortChar)        );
    println( "J) f() 的大小:                        ", sizeof f()                   );
    println( "K) Base::a 的大小:                    ", sizeof Base::a               );
 
//  println( "函数的大小:     ", sizeof(void()) ); // 错误
//  println( "不完整类型的大小:", sizeof(int[])  ); // 错误
//  println( "位域的大小:     ", sizeof bit.bit ); // 错误
}

可能的输出:

1) 空类的大小:                        1
2) 指针的大小:                        8
3) sizeof(Bit)    -类:               4
4) sizeof(int[10])-含有10个 int 的数组:40
5) sizeof a       -含有10个 int 的数组:40
6) 含有10个 int 的数组的长度:           10
7) 含有10个 int 的数组的长度 (2):       10
8) Derived 的大小:                    8
9) 通过 Base 获取 Derived 的大小:      4
A) sizeof(unsigned):                 4
B) sizeof(int):                      4
C) sizeof(short):                    2
D) sizeof(char):                     1
E) sizeof(CharChar):                 2
F) sizeof(CharCharInt):              8
G) sizeof(IntCharChar):              8
H) sizeof(CharIntChar):              12
I) sizeof(CharShortChar):            6
J) f() 的大小:                        1
K) Base::a 的大小:                    4    
  • alignof
https://zh.cppreference.com/w/cpp/language/alignof
查询类型的对齐要求。
语法
alignof( 类型标识 )		
返回 std::size_t 类型的值。
解释
返回由类型标识所指示的类型的任何实例所要求的对齐字节数,该类型可以是完整对象类型、元素类型完整的数组类型或者到这些类型之一的引用类型。
如果类型是引用类型,那么运算符返回被引用类型的对齐要求;如果类型是数组类型,那么返回元素类型的对齐要求。
    
#include <iostream>
 
struct Foo
{
    int   i;
    float f;
    char  c;
};
 
// 注:下面的 `alignas(alignof(long double))`
// 如果需要可以简化为 `alignas(long double)`
struct alignas(alignof(long double)) Foo2
{
    // Foo2 成员的定义...
};
 
struct Empty {};
 
struct alignas(64) Empty64 {};
 
int main()
{
    std::cout << "对齐字节数"  "\n"
        "- char                :" << alignof(char)    << "\n"
        "- 指针                :" << alignof(int*)     << "\n"
        "- Foo 类              :" << alignof(Foo)      << "\n"
        "- Foo2 类             :" << alignof(Foo2)     << "\n"
        "- 空类                :" << alignof(Empty)    << "\n"
        "- 带 alignas(64) 的空类:" << alignof(Empty64) << "\n";
}

可能的输出:
对齐字节数
- char                :1
- 指针                :8
- Foo 类              :4
- Foo2 类             :16
- 空类                :1
- 带 alignas(64) 的空类:64
  • typeid
https://zh.cppreference.com/w/cpp/language/typeid
查询类型的信息。
用于必须知晓多态对象的动态类型的场合以及静态类型鉴别。
语法
typeid ( 类型 )	(1)	
typeid ( 表达式 )	(2)	
在使用 typeid 前必须包含或导入 (C++20 起)标头 <typeinfo>,否则程序非良构。

(C++23 前)
所有 typeid 表达式前都必须有 std::type_info 的标准库声明,否则程序非良构。std::type_info 的标准库声明在标头 <typeinfo> 以及标准库模块 std 和 std.compat 中提供。

(C++23 起)
typeid 表达式是左值表达式,它指代一个具有静态存储期的,多态类型 std::type_info 或它的某个派生类型的 const 限定版本类型的对象。

#include <iostream>
#include <string>
#include <typeinfo>
 
struct Base {}; // 非多态
struct Derived : Base {};
 
struct Base2 { virtual void foo() {} }; // 多态
struct Derived2 : Base2 {};
 
int main()
{
    int myint = 50;
    std::string mystr = "string";
    double *mydoubleptr = nullptr;
 
    std::cout << "myint 的类型:" << typeid(myint).name() << '\n'
              << "mystr 的类型:" << typeid(mystr).name() << '\n'
              << "mydoubleptr 的类型:" << typeid(mydoubleptr).name() << '\n';
 
    // std::cout << myint 是多态类型的泛左值表达式;求值
    const std::type_info& r1 = typeid(std::cout << myint); // 副作用:打印 50
    std::cout << '\n' << "std::cout<<myint 的类型:" << r1.name() << '\n';
 
    // std::printf() 不是多态类型的泛左值表达式;不求值
    const std::type_info& r2 = typeid(std::printf("%d\n", myint));
    std::cout << "printf(\"%d\\n\",myint) 的类型:" << r2.name() << '\n';
 
    // 非多态左值时是静态类型
    Derived d1;
    Base& b1 = d1;
    std::cout << "非多态基类的引用:" << typeid(b1).name() << '\n';
 
    Derived2 d2;
    Base2& b2 = d2;
    std::cout << "多态基类的引用:" << typeid(b2).name() << '\n';
 
    try
    {
        // 解引用空指针:对于非多态表达式 OK
        std::cout << "mydoubleptr 指向 " << typeid(*mydoubleptr).name() << '\n'; 
        // 解引用空指针:对多态左值则不行
        Derived2* bad_ptr = nullptr;
        std::cout << "bad_ptr 指向...";
        std::cout << typeid(*bad_ptr).name() << '\n';
    }
    catch (const std::bad_typeid& e)
    {
         std::cout << " 捕获 " << e.what() << '\n';
    }
}
可能的输出:

======== 来自 Clang 的输出 ========
myint 的类型:i
mystr 的类型:NSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
mydoubleptr 的类型:Pd
50
std::cout<<myint 的类型:NSt3__113basic_ostreamIcNS_11char_traitsIcEEEE
printf("%d\n",myint) 的类型:i
非多态基类的引用:4Base
多态基类的引用:8Derived2
mydoubleptr 指向 d
bad_ptr 指向... 捕获 std::bad_typeid
 
======== 来自 MSVC 的输出 ========
myint 的类型:int
mystr 的类型:class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >
mydoubleptr 的类型:double * __ptr64
50
std::cout<<myint 的类型:class std::basic_ostream<char,struct std::char_traits<char> >
printf("%d\n",myint) 的类型:int
非多态基类的引用:struct Base
多态基类的引用:struct Derived2
mydoubleptr 指向 double
bad_ptr 指向... 捕获 Attempted a typeid of nullptr pointer!    
  • throw 表达式
https://zh.cppreference.com/w/cpp/language/throw
对错误条件发信号,并执行错误处理代码。
语法
throw 表达式	(1)	
throw	(2)	
解释
更多关于 try 与 catch(异常处理)块的信息见 try-catch 块
1) 首先,从表达式??复制初始化异常对象:
这可能会调用右值表达式的移动构造函数。即使复制初始化选择了移动构造函数,从左值复制初始化仍必须为良式,且析构函数必须可访问
(C++11 起)
这也可能会通过如 return 语句中一般的两步重载决议调用左值表达式的移动构造函数,如果它们指名局部变量或是函数或 catch 子句的形参,且它的作用域不会超出最内层的外围 try 块(如果存在)
(C++17 起)
复制/移动 (C++11 起)可能会被复制消除处理
然后转移控制给拥有匹配类型,所在的复合语句或成员初始化器列表是最近进入,且未由此执行线程退出的异常处理块。
2) 重抛当前处理的异常。中止当前 catch 块的执行并将控制转移到下一个匹配的异常处理块(但不是到同一个 try 块的下个 catch 子句:它所在的复合语句被认为已经‘退出’),并重用既存的异常对象:不会生成新对象。只能在异常处理过程中使用这种形式(其他情况中使用时会调用 std::terminate)。对于构造函数,关联到函数 try 块 的 catch 子句必须通过重抛出退出。
关于在异常处理期间引发错误,见 std::terminate 与 std::unexpected (C++17 前)。

#include <iostream>
#include <stdexcept>
 
struct A
{
    int n;
 
    A(int n = 0): n(n) { std::cout << "A(" << n << ") 已成功构造\n"; }
    ~A() { std::cout << "A(" << n << ") 已销毁\n"; }
};
 
int foo()
{
    throw std::runtime_error("错误");
}
 
struct B
{
    A a1, a2, a3;
 
    B() try : a1(1), a2(foo()), a3(3)
    {
        std::cout << "B 已成功构造\n";
    }
    catch(...)
    {
    	std::cout << "B::B() 因异常退出\n";
    }
 
    ~B() { std::cout << "B 已摧毁\n"; }
};
 
struct C : A, B
{
    C() try
    {
        std::cout << "C::C() 已成功完成\n";
    }
    catch(...)
    {
        std::cout << "C::C() 因异常退出\n";
    }
 
    ~C() { std::cout << "C 已销毁\n"; }
};
 
int main () try
{
    // 创建 A 基类子对象
    // 创建 B 的成员 a1
    // 创建 B 的成员 a2 失败
    // 回溯销毁 B 的 a1 成员
    // 回溯销毁 A 基类子对象
    C c;
}
catch (const std::exception& e)
{
    std::cout << "main() 创建 C 失败,原因:" << e.what();
}
输出:

A(0) 已成功构造
A(1) 已成功构造
A(1) 已销毁
B::B() 因异常退出
A(0) 已销毁
C::C() 因异常退出
main() 创建 C 失败,原因:错误    

Tags:

最近发表
标签列表