目录
一、noexcept说明符与类成员函数
c++11起,标准库开起引入noexcept说明符,旨在替代throw,用来指定函数是否抛出异常。
1.1 noexcept语法及应用
noexcept 说明符的语法形式如下:
/*与 noexcept(true) 相同*/
noexcept
/*如果 表达式 求值为 true,那么声明函数不会抛出任何异常。
*表达式 - 按语境转换为 bool 类型的常量表达式 */
noexcept(表达式)
/*与 noexcept(true) 相同(C++17 前的语义见动态异常说明)
*(C++17 起)(C++17 中弃用)(C++20 中移除) */
throw()
noexcept 说明是函数类型的一部分,可以作为任何函数声明符的一部分出现。 noexcept 说明的每个函数要么不会抛出,要么有可能会抛出。
#include <iostream>
int f1() noexcept;
int f2() noexcept(false) { return 0; }
int f3(int val) noexcept(__cplusplus > 201103L){ return val/2; }
// f4 是否声明为 noexcept 取决于 T() 是否会抛出异常
template <class T> int f4() noexcept(noexcept(T())) { return 0;}
noexcept 也可作为运算符使用,进行编译时检查,成为表达式的一部分,如果表达式不会抛出任何异常则返回 true。noexcept 运算符不会对 表达式 求值。如果 表达式 的潜在异常集合为空 (C++17 前)表达式 不会抛出 (C++17 起),那么结果是 true,否则结果是 false。
void noexcept_test(void)
{
bool b1 = noexcept(f1()); //noexcept作为运算符使用
std::cout << "b1 = " << b1 << "\n" ;// true
bool b2 = noexcept(f2());
std::cout << "b2 = " << b2 << "\n" ;// false
bool b3 = noexcept(f3(10));
std::cout << "b3 = " << b3 << "\n" ;// true,c++11后,false,c++11
bool b4 = noexcept(f4<int>());
std::cout << "b4 = " << b4 << "\n" ;// true
}
当为一个函数添加noexcept 说明时,即noexcept(true),该函数将不抛出异常,哪怕f1仅仅声明却没有定义;而对函数f2添加noexcept(false)说明时,哪怕该函数就一个简单的一目了然功能,也会抛出异常。当函数添加的是noexcept (表达式)说明时,是否抛出异常由表达式计算结果来决定。表达式是能转换为boo结果的常量表达式。
//表达式必须是按语境转换为 bool 类型的常量表达式
int f5(int val) noexcept(val>10) { return 0; } //error,只能是常量表达式,不能使用参数
template<class T> T f6() noexcept(sizeof(T) < 4); //OK
1.2 noexcept 与类设计
noexcept 说明在类中使用,不违反类的定义及使用情况下,可以通过noexcept 说明规避定义工作。
class A1
{
private:
/* data */
int ival;
public:
A1(int ival_) : ival(ival_){}; //
A1(/* args */) noexcept; //由于有其他构造函数,该构造函数可以不定义,类似于=delete
// ~A1() noexcept; //编译错误,不能和普通函数一样不定义
~A1() noexcept{}; //
void f(int ival_) noexcept;
};
void noexcept_test(void)
{
A1 a1(10);
// a1.f(1); //error,函数未定义
decltype(a1.f(1)) *p; //OK, f 不求值调用,但需要 noexcept 说明
}
只有异常说明不同的函数不能重载(与返回类型相似,异常说明是函数类型的一部分,但不是函数签名的一部分) (C++17 起)。
class A1
{
private:
/* data */
int ival;
public:
//其他...
void f1() noexcept;
// void f1(){}; // 错误,不能重载
void f2() noexcept(false);
// void f2(){}; // 错误,不能重载
};
1.3 noexcept 与成员函数指针
指向不会抛出的函数的指针(包括成员函数指针)能赋值给或用以初始化 (C++17 前)可以隐式转换到 (C++17 起)指向有可能会抛出的函数的指针,但反之不可。
class A1
{
private:
/* data */
int ival;
public:
//其他...
//
void (*pf3)() noexcept;
void (*pf4)();
};
void fa3() noexcept;
void fa4() {};
void fa5() noexcept{};
//
A1 a1(10);
a1.pf3 = fa3; //error,指向未定义函数
a1.pf3 = fa4; //error,要求收窄
a1.pf3 = fa5; //OK
a1.pf4 = fa3; //error,指向未定义函数
a1.pf4 = fa4; //OK
a1.pf4 = fa5; //OK
因此将类函数成员指针指向外部函数时,带noexcept 说明的必须指向定义为noexcept 说明的函数,而不带noexcept 说明的函数指针可指向两者。
1.4 noexcept 与成员函数调用
不会抛出的函数允许调用有可能会抛出的函数。每当抛出异常且对处理块的查找遇到了不会抛出的函数的最外层块时,就调用函数 std::terminate 或 std::unexpected (C++17 前):
class A1
{
private:
/* data */
int ival;
public:
//other...
void f3(){};
void g1() noexcept
{
f3(); // 合法,即使 f 抛出异常,等效于调用 std::terminate
}
// void f4() noexcept; //调用时未定义
void f4() noexcept{};
void g2()
{
f4(); // 合法,严格收窄
}
};
//
A1 a1(10);
a1.g1();
a1.g2();
1.5 noexcept 与虚函数
如果虚函数带noexcept 说明,指出其不会抛出,那么它每个覆盖的函数的所有声明(包括定义)都必须不抛出,除非覆盖函数被定义为弃置:
class Base_noe {
public:
virtual void f() noexcept;
virtual void g();
virtual void h() noexcept = delete;
};
class Derived_noe: public Base_noe {
public:
// void f(); // 谬构:Derived_noe::f 有可能会抛出,Base_noe::f 不会抛出
void g() noexcept; // OK,收窄
// void h() = delete; // error,overridden function
};
1.6 noexcept 与常量表达式要求
表达式 e 有可能会抛出:
- e 是对有可能会抛出的函数、函数指针或成员函数指针的调用,除非e是核心常量表达式 (C++17 前)
- e 对有可能会抛出的函数进行隐式调用(例如作为重载运算符,new 表达式中的分配函数,函数实参的构造函数,或当 e 为完整表达式时的析构函数)
- e 是 throw 表达式
- e 是对多态引用类型进行转型的 dynamic_cast
- e 是应用于指向多态类型的指针的解引用的 typeid 表达式
- e 包含有可能会抛出的直接子表达式
class Base_noe1 {
public:
Base_noe1(int = (Base_noe1(5), 0)) noexcept;
Base_noe1(const Base_noe1&) noexcept;
Base_noe1(Base_noe1&&) noexcept;
~Base_noe1();
private:
int ival;
};
int K(){return 1;}
class Base_noe2 {
public:
Base_noe2() throw();
Base_noe2(const Base_noe2&) = default; // 隐式异常说明是 noexcept(true)
Base_noe2(Base_noe2&&, int = (throw K(), 0)) noexcept;
~Base_noe2() noexcept(false);
private:
char cval;
};
class Derived_noe12 : public Base_noe1, public Base_noe2 {
public:
// Derived_noe12::Derived_noe12() 有可能会抛出,因为 new 运算符
// Derived_noe12::Derived_noe12(const Derived_noe12&) 不会抛出
// Derived_noe12::Derived_noe12(D&&) 有可能会抛出:因为 Base_noe2 的构造函数的默认实参有可能会抛出
// Derived_noe12::~Derived_noe12() 有可能会抛出
// 注意:如果 Base_noe1::~Base_noe1() 为虚,那么此程序将为非良构,因为不会抛出的虚函数的覆盖函数不能抛出
private:
double dval;
};
函数上的 noexcept 说明不是一种编译时检查;它只不过是程序员告知编译器函数是否可以抛出异常的一种方法。编译器能用此信息启用不会抛出的函数上的某些优化,以及启用能在编译时检查特定表达式是否声明为可抛出任何异常的 noexcept 运算符。例如,诸如 std::vector 的容器会在元素的移动构造函数是 noexcept 的情况下移动元素,否则就复制元素(除非复制构造函数不可访问,但有可能会抛出的移动构造函数只会在放弃强异常保证的情况下考虑)。
二 、空类
2.1 空类不空
空类定义就是关键词class或struct加上空内容体{}定义:
class Empty{};
struct Empty{};
当C++编译器通过它的时候。如果你没有声明下列函数,体贴的编译器会声明它自己的版本。这些函数是:一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取址运算符。另外,如果你没有声明任何构造函数,它也将为你声明一个缺省构造函数。所有这些函数都是公有的。
class Empty{};
//被编译器扩充为
class Empty {
public:
Empty(); // 缺省构造函数
Empty(const Empty& rhs); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=(const Empty& rhs); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const;
};
因此,该空类可以先大多数普通类一样来使用,为保证同一类型的不同对象地址始终有别,要求任何对象或成员子对象的大小至少为 1,即使该类型是空的类类型。
Empty b1, b2; //构造
b1 = b2; //复制赋值
b1 = std::move(b2);//移动赋值
Empty *pb1=new Empty(), *pb2 = nullptr; //构造
pb2 = pb1; //
pb2 = &b1; //
*pb2 = b2; //复制赋值
pb2 = nullptr;
delete pb1; //析构
pb1 = nullptr;
// 任何空类类型的对象大小至少为 1
std::cout <<"sizeof(Empty) = "<< sizeof(Empty)<<"\n"; //1
2.2 空基类优化
虽然规定空类类型大小为 1,当它作为基类子对象时,有时不受这种制约,而且可以完全从对象布局中被优化掉
//为保证同一类型的不同对象地址始终有别,要求任何对象或成员子对象的大小至少为 1,即使该类型是空的类类型
struct Base {}; // 空类,占用1字节
//如果首个非静态数据成员的类型与一个空基类的类型相同或者由该空基类派生,
//那么禁用空基类优化,因为要求两个同类型基类子对象在最终派生类型的对象表示中必须拥有不同地址。
struct Derived1 : Base { //空基类被优化
int i; // int成员占用 sizeof(int) 字节
};
//
// 任何空类类型的对象大小至少为 1
std::cout <<"sizeof(Base) = "<< sizeof(Base)<<"\n";//1
// 应用空基类优化
std::cout <<"sizeof(Derived1) = "<< sizeof(Derived1) <<"\n";//4
如果首个非静态数据成员的类型与一个空基类的类型相同或者由该空基类派生,那么禁用空基类优化,因为要求两个同类型基类子对象在最终派生类型的对象表示中必须拥有不同地址。
//为保证同一类型的不同对象地址始终有别,要求任何对象或成员子对象的大小至少为 1,即使该类型是空的类类型
struct Base {}; // 空类,占用1字节
//基类从对象布局中被优化掉
struct Derived : Base { }; //空基类被优化
//如果首个非静态数据成员的类型与一个空基类的类型相同或者由该空基类派生,
//那么禁用空基类优化,因为要求两个同类型基类子对象在最终派生类型的对象表示中必须拥有不同地址。
struct Derived1 : Base { //空基类被优化
int i; // int成员占用 sizeof(int) 字节
};
struct Derived2 : Base {// 不应用空基优化,基类占用1字节
Base c; //Base 成员占用1字节,后随2个填充字节以满足 int 的对齐要求
int i;
};
struct Derived3 : Base {//不应用空基类优化,基类占用至少 1 字节加填充
Derived1 c; // Derived1成员占用 sizeof(int) 字节
int i; // int成员占用 sizeof(int) 字节
};
struct Derived4 : Base {// //空基类被优化
int i; // int成员占用 sizeof(int) 字节
Base c; //Base 成员占用1字节,后随3个填充字节以满足 int 的对齐要求
};
//
// 任何空类类型的对象大小至少为 1
std::cout <<"sizeof(Base) = "<< sizeof(Base)<<"\n";//1
// 应用空基类优化
std::cout <<"sizeof(Derived) = "<< sizeof(Derived) <<"\n"; //1
std::cout <<"sizeof(Derived1) = "<< sizeof(Derived1) <<"\n";//4
std::cout <<"sizeof(Derived2) = "<< sizeof(Derived2) <<"\n";//8
std::cout <<"sizeof(Derived3) = "<< sizeof(Derived3) <<"\n";//12
std::cout <<"sizeof(Derived4) = "<< sizeof(Derived4) <<"\n";//8
2.3 [[no_unique_address]]优化空类
如果空成员子对象使用属性 [[no_unique_address]],那么允许像空基类一样优化掉它们。取这种成员的地址会产生可能等于同一个对象的某个其他成员的地址。
//为保证同一类型的不同对象地址始终有别,要求任何对象或成员子对象的大小至少为 1,即使该类型是空的类类型
struct Base {}; // 空类,占用1字节
//如果空成员子对象使用属性 [[no_unique_address]],那么允许像空基类一样优化掉它们。
struct Base1 {
[[no_unique_address]] Base c1; //主动优化,空成员c1被优化掉
Base c2; // Base,占用 1 字节,后随对 i 的填充
int i; // int成员占用 sizeof(int) 字节
};
//
// 应用空基类优化
std::cout <<"sizeof(Base1) = "<< sizeof(Base1) <<"\n"; //8
而[[no_unique_address]]优化本质上是通过与其他既定成员的共享地址来实现的:
struct Empty {}; // 空类
struct X {
int i;
Empty e; //Empty成员占用1字节,后随3个填充字节以满足 int 的对齐要求
};
struct Y {
int i;
[[no_unique_address]] Empty e;//主动优化,空成员e被优化掉
};
struct Z {
char c;
//e1 与 e2 不能共享同一地址,因为它们拥有相同类型,其中一者可以与 c 共享地址
[[no_unique_address]] Empty e1, e2;//主动优化,空成员e1、e2其中一个被优化掉
};
struct W {
char c[2];
//e1 与 e2 不能拥有同一地址,但它们之一能与 c[0] 共享,而另一者与 c[1] 共享
[[no_unique_address]] Empty e1, e2;//主动优化,空成员e1、e2被优化掉
};
//
void no_unique_address_test(void)
{
// 任何空类类型对象的大小至少为 1
std::cout <<"sizeof(Empty) = "<< sizeof(Empty) <<"\n"; //1
// 至少需要多一个字节以给 e 唯一地址
std::cout <<"sizeof(X) = "<< sizeof(X) <<"\n"; //8
// 优化掉空成员
std::cout <<"sizeof(Y) = "<< sizeof(Y) <<"\n";//4
// e1 与 e2 不能共享同一地址,因为它们拥有相同类型,尽管它们标记有 [[no_unique_address]]。
// 然而,其中一者可以与 c 共享地址。
std::cout <<"sizeof(Z) = "<< sizeof(Z) <<"\n";//2
// e1 与 e2 不能拥有同一地址,但它们之一能与 c[0] 共享,而另一者与 c[1] 共享
std::cout <<"sizeof(W) = "<< sizeof(W) <<"\n";//3
};
三、explicit说明符
3.1 explicit语法形式
explicit 说明符只能在类定义之内的构造函数或转换函数 (C++11 起)的 声明说明符序列中出现, explicit语法形式:
/*
*指定构造函数或转换函数 (C++11 起)或推导指引(C++17 起)为显式,
*即它不能用于隐式转换和复制初始化。
*/
explicit
/*explicit 说明符可以与常量表达式一同使用。
*函数在且只会在该常量表达式求值为 true 时是显式的。 (C++20 起)
*表达式 - 经按语境转换为 bool 类型的常量表达式
*/
explicit ( 表达式 ) //(C++20 起)
声明时不带函数说明符 explicit 的拥有单个无默认值形参的 (C++11 前)构造函数被称作转换构造函数(converting constructor)。
3.2 转换构造函数
与只在直接初始化(包括如 static_cast 这样的显式转换)中被考虑的显式构造函数不同,转换构造函数也会作为用户定义的转换序列中的一部分而在复制初始化的考虑范围内。通常说法是转换构造函数指定了一个从它的实参类型(如果存在)到它的类类型的隐式转换。注意非显式的用户定义转换函数也指定了一个隐式转换。隐式声明的及用户定义的非显式复制构造函数与移动构造函数也是转换构造函数。
struct A_common
{
A_common(int) { std::cout <<"A_common(int)\n";} // 转换构造函数
A_common(int, int) { std::cout <<"A_common(int,int)\n";} // 转换构造函数(C++11)
operator bool() const { std::cout <<"A_common bool()\n";return true; }
};
构造函数(除了复制或移动)和用户定义转换函数都可以是函数模板;添加了explicit 的含义不变。
//explicit 说明符只能在类定义之内的构造函数或转换函数 (C++11 起)的 声明说明符序列中出现。
struct B_explicit
{
explicit B_explicit(int) { std::cout <<"B_explicit(int)\n";}
explicit B_explicit(int, int) { std::cout <<"B_explicit(int,int)\n";}
explicit operator bool() const { std::cout <<"B_explicit bool()\n";return true; }
};
3.3 explicit使用约束
explicit关键字主要有两个用途:
- 指定构造函数或转换函数 (C++11起)为显式, 即它不能用于隐式转换和复制初始化。
- 可以与常量表达式一同使用. 当该常量表达式为 true 才为显式转换(C++20起)。
void explicit_test(void)
{
A_common a1 = 1; // OK:复制初始化选择 A_common::A_common(int)
A_common a2(2); // OK:直接初始化选择 A_common::A_common(int)
A_common a3 {4, 5}; // OK:直接列表初始化选择 A_common::A_common(int, int)
A_common a4 = {4, 5}; // OK:复制列表初始化选择 A_common::A_common(int, int)
A_common a5 = (A_common)1; // OK:显式转型进行 static_cast
if (a1) ; // OK:A_common::operator bool()
bool na1 = a1; // OK:复制初始化选择 A_common::operator bool()
bool na2 = static_cast<bool>(a1); // OK:static_cast 进行直接初始化
// B_explicit b1 = 1; // 错误:复制初始化不考虑 B_explicit::B_explicit(int)
B_explicit b2(2); // OK:直接初始化选择 B_explicit::B_explicit(int)
B_explicit b3 {4, 5}; // OK:直接列表初始化选择 B_explicit::B_explicit(int, int)
// B_explicit b4 = {4, 5}; // 错误:复制列表初始化不考虑 B_explicit::B_explicit(int,int)
B_explicit b5 = (B_explicit)1; // OK:显式转型进行 static_cast
if (b2) ; // OK:B_explicit::operator bool()
// bool nb1 = b2; // 错误:复制初始化不考虑 B_explicit::operator bool()
bool nb2 = static_cast<bool>(b2); // OK:static_cast 进行直接初始化
}
3.4 explicit与常量表达式
在c++20标准起,explicit关键字可以和表达式组成一起使用,依据表达式返回值来确定是否需要显示构造。
//explicit 说明符可以与常量表达式一同使用。函数在且只会在该常量表达式求值为 true 时是显式的。
explicit ( 表达式 ) //(C++20 起)
explicit关键字可以与常量表达式constexpr一起使用。但是,如果该常量表达式的计算结果为true,此时构造函数是显式的。
constexpr bool cexpr1 = false;
constexpr bool cexpr2 = true;
class C_explicit {
public:
explicit(cexpr1) C_explicit(int) {}; //c++20
};
class D_explicit {
public:
explicit(cexpr2) D_explicit(int) {}; //c++20
};
//
C_explicit ec1=1; //OK
C_explicit ec2(1);
// D_explicit ec3=1; //error
D_explicit ec4(1);
四、constexpr说明符
4.1 constexpr 说明符与const
C++11 起,提供了constexpr 说明符, 指定变量或函数的值可以在常量表达式中出现。constexpr 说明符声明编译时可以对函数或变量求值值。这些变量和函数(给定了合适的函数实参的情况下)即可用于需要编译期常量表达式的地方。
// 输出要求编译时常量的函数,用于测试
template<int n>
class constN
{
public:
constN() { std::cout << n << '\n'; }
};
//
int number = 10;
constN<number> cnumber; //error,不能作为常量表达
constexpr int num = 10;
constN<num> cnum; // OK,在编译时计算
const int numc = 10;
constN<numc> cnumc; //OK
咋一看,和const没啥区别,但是C++ 11标准中,为了解决 const 关键字的双重语义问题,保留了 const 表示“只读”的语义,而将“常量”的语义划分给了新添加的 constexpr 关键字。因此建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。
constexpr int sqr1(int arg){
return arg*arg;
}
const int sqr2(int arg){
return arg*arg;
}
#include <array>
//
std::array<int,sqr1(3)> a1;
// std::array<int,sqr2(3)> a2;//error: call to non-'constexpr' function 'const int sqr2(int)'
上述代码,因为 sqr2() 函数的返回值仅有 const 修饰,而没有用更明确的 constexpr 修饰,导致其无法用于初始化 array 容器(只有常量才能初始化 array 容器)。
4.2 constexpr使用要求
constexpr定义的变量必须立即被初始化,它的初始化包括所有隐式转换、构造函数调用等的全表达式必须是常量表达式:
- 它的类型必须是字面类型 (LiteralType) 。
- 它必须被立即初始化。
- 它的初始化包括所有隐式转换、构造函数调用等的全表达式必须是常量表达式
- 它必须拥有常量析构
// C++11 constexpr 函数使用递归而非迭代
// C++14 constexpr 函数可使用局部变量和循环
constexpr int factorial(int n)
{
return n <= 1 ? 1 : (n * factorial(n - 1));
}
//
constexpr int num1; //error: uninitialized 'const num1' [-fpermissive]
num1 = 100; //error: assignment of read-only variable 'num1'
constexpr int num = 10;
constexpr int num2 = 2*num; //OK
constexpr int num3 = factorial(3); //OK
constexpr定义的函数需要满足以下条件:
【1】它必须非虚,非虚常量不可达, (C++20 前)
class constexprTest
{
public:
constexpr virtual void f(){}; //c++20前,error,在此模式下,函数不能同时为 constexpr 和 virtual
};
【2】它必须不是协程,协程通常常量不可达, (C++20 起)
#include <coroutine>
class constexprTest
{
public:
constexpr task coroutine_func(int n = 0) //error: 'co_return' cannot be used in a 'constexpr' function
{
co_return;
}
};
【3】它的返回类型(如果存在)必须是字面类型 (LiteralType)
class Empty{};
class Test1
{
private:
char* pc;
public:
Test1(/* args */){pc = new char[10];};
~Test1(){delete[] pc; pc = nullptr;};
};
//
class constexprTest
{
public:
constexpr Empty f() //OK
{
return Empty();
}
constexpr Test1 g()//error: invalid return type 'Test1' of 'constexpr' function 'constexpr Test1 constexprTest::g()'
{
return Test1();
}
};
【4】它的每个参数都必须是字面类型 (LiteralType)
【5】对于构造函数与析构函数 (C++20 起),该类必须无虚基类
lass constexprTest : virtual Empty //虚继承影响constexpr的构造和析构
{
public:
constexpr constexprTest(){}; //error: 'class constexprTest' has virtual base classes
constexpr ~constexprTest(){}; //c++20前,析构函数不能是 constexpr
};
【6】至少存在一组实参值,使得函数的一个调用为核心常量表达式的被求值的子表达式(对于构造函数为足以用于常量初始化器) (C++14 起)。不要求诊断是否违反这点。
【7】它的函数体必须不是函数 try 块 (C++20 前)
class constexprTest
{
public:
constexpr void g3(){
try //error 语句不能出现在 constexpr 函数中
{
/* code */
}
catch(const std::exception& e)
{
std::cerr << e.what() << '\n';
}
}
};
【8】函数体必须被弃置或预置,或只含有下列内容(C++14 前):1)空语句(仅分号);2)static_assert 声明;3)不定义类或枚举的 typedef 声明及别名声明;4)using 声明;5)using 指令;6)恰好一条 return 语句,当函数不是构造函数时。
◦函数体必须不含 (C++14 起,C++203前):1)goto 语句;2)拥有除 case 和 default 之外的带有标签的语句;3)(C++20前)try 块;4)(C++20前)asm 声明;5)(C++20前)不进行初始化的变量定义;6)非字面类型的变量定义;7)静态或线程存储期变量的定义;8)(是 =default; 或 =delete; 的函数体均不含任何上述内容。)
【9】函数体非 =delete; 的 constexpr 构造函数必须满足下列额外要求:1)对于类或结构体的构造函数,每个子对象和每个非变体非静态数据成员必须被初始化。若类是联合体式的类,对于其每个非空匿名联合体成员,必须恰好有一个变体成员被初始化(C++20前);2)对于非空联合体的构造函数,恰好有一个非静态数据成员被初始化 (C++20 前);3)每个被选用于初始化非静态成员和基类的构造函数必须是 constexpr 构造函数。
【10】析构函数不能为 constexpr,但能在常量表达式中隐式调用平凡析构函数。 (C++20 前)
【11】函数体非 =delete; 的 constexpr 析构函数必须满足下列额外要求:◦每个用于销毁非静态数据成员与基类的析构函数必须是 constexpr 析构函数。(C++20起)
4.3 constexpr运用
请记住,只要有constexpr出现的地方,其必然为常量表达式可达,即能编译期能计算常量结果,如果修饰变量就是变量为常量可达,如果修饰函数就返回、形参常量可达,以及函数体可确定。
#include <stdexcept>
// C++11 constexpr 函数使用递归而非迭代
// C++14 constexpr 函数可使用局部变量和循环
constexpr int factorial(int n)
{
return n <= 1 ? 1 : (n * factorial(n - 1));
}
// 输出要求编译时常量的函数,用于测试
template<int n>
class constN
{
public:
constN() { std::cout << n << '\n'; }
};
// 字面类
class conststr {
const char* p;
std::size_t sz;
public:
template<std::size_t N>
constexpr conststr(const char(&a)[N]): p(a), sz(N - 1) {}
// constexpr 函数通过抛异常来提示错误
// C++11 中,它们必须用条件运算符 ?: 来这么做
constexpr char operator[](std::size_t n) const
{
return n < sz ? p[n] : throw std::out_of_range("");
}
constexpr std::size_t size() const { return sz; }
};
// C++11 constexpr 函数必须把一切放在单条 return 语句中
// (C++14 无此要求)
constexpr std::size_t countlower(conststr s, std::size_t n = 0,
std::size_t c = 0)
{
return n == s.size() ? c :
'a' <= s[n] && s[n] <= 'z' ? countlower(s, n + 1, c + 1) :
countlower(s, n + 1, c);
};
//
std::cout << "4! = " ;
constN<factorial(4)> out1; // 在编译时计算
volatile int k = 8; // 使用 volatile 防止优化
std::cout << k << "! = " << factorial(k) << '\n'; // 运行时计算
std::cout << "the number of lowercase letters in \"Hello, world!\" is ";
constN<countlower("Hello, world!")> out2; // 隐式转换到常量字符串
//out log
4! = 24
8! = 40320
the number of lowercase letters in "Hello, world!" is 9
本文介绍结束!
感谢读友能耐心看到最后,本人文章都很长,知识点很细,可能会有所遗漏和失误,如果有不妥之处,烦请指出。如果内容对您有所触动,请点赞关注一下防止不迷路。
五、源码补充
编译指令:g++ main.cpp test*.cpp -o test.exe -std=c++20(或c++11、c++17)
main.cpp
#include "test1.h"
int main(int argc, char* argv[])
{
noexcept_test();
compile_test();
empty_class_test();
no_unique_address_test();
explicit_test();
return 0;
}
test1.h
#ifndef _TEST_1_H_
#define _TEST_1_H_
void noexcept_test(void);
void compile_test(void);
void empty_class_test(void);
void no_unique_address_test(void);
void explicit_test(void);
#endif //_TEST_1_H_
test1.cpp
#include "test1.h"
#include <iostream>
int f1() noexcept;
int f2() noexcept(false) { return 0; }
int f3(int val) noexcept(__cplusplus > 201103L){ return val/2; }
// f4 是否声明为 noexcept 取决于 T() 是否会抛出异常
template <class T> int f4() noexcept(noexcept(T())) { return 0;}
// int f5(int val) noexcept(val>10) { return 0; } //error,只能是常量表达式,不能使用参数
template<class T> T f6() noexcept(sizeof(T) < 4); //OK
class A1
{
private:
/* data */
int ival;
public:
A1(int ival_) : ival(ival_){}; //
A1(/* args */) noexcept;
// ~A1() noexcept; //编译错误,不能和普通函数一样不定义
~A1() noexcept{}; //
void f(int ival_) noexcept;
//
void f1() noexcept;
// void f1(){}; // 错误:不算重载
void f2() noexcept(false);
// void f2(){}; // 错误:不算重载
//
void (*pf3)() noexcept;
void (*pf4)();
void f3(){};
void g1() noexcept
{
f3(); // 合法,即使 f 抛出异常,等效于调用 std::terminate
}
// void f4() noexcept; //调用时未定义
void f4() noexcept{};
void g2()
{
f4(); // 合法,严格收窄
}
};
void fa3() noexcept;
void fa4() {};
void fa5() noexcept{};
class Base_noe {
public:
virtual void f() noexcept;
virtual void g();
virtual void h() noexcept = delete;
};
class Derived_noe: public Base_noe {
public:
// void f(); // 谬构:Derived_noe::f 有可能会抛出,Base_noe::f 不会抛出
void g() noexcept; // OK,收窄
// void h() = delete; // error,overridden function
};
class Base_noe1 {
public:
Base_noe1(int = (Base_noe1(5), 0)) noexcept;
Base_noe1(const Base_noe1&) noexcept;
Base_noe1(Base_noe1&&) noexcept;
~Base_noe1();
private:
int ival;
};
int K(){return 1;}
class Base_noe2 {
public:
Base_noe2() throw();
Base_noe2(const Base_noe2&) = default; // 隐式异常说明是 noexcept(true)
Base_noe2(Base_noe2&&, int = (throw K(), 0)) noexcept;
~Base_noe2() noexcept(false);
private:
char cval;
};
class Derived_noe12 : public Base_noe1, public Base_noe2 {
public:
// Derived_noe12::Derived_noe12() 有可能会抛出,因为 new 运算符
// Derived_noe12::Derived_noe12(const Derived_noe12&) 不会抛出
// Derived_noe12::Derived_noe12(D&&) 有可能会抛出:因为 Base_noe2 的构造函数的默认实参有可能会抛出
// Derived_noe12::~Derived_noe12() 有可能会抛出
// 注意:如果 Base_noe1::~Base_noe1() 为虚,那么此程序将为非良构,因为不会抛出的虚函数的覆盖函数不能抛出
private:
double dval;
};
void noexcept_test(void)
{
bool b1 = noexcept(f1());
std::cout << "b1 = " << b1 << "\n" ;// true
bool b2 = noexcept(f2());
std::cout << "b2 = " << b2 << "\n" ;// false
bool b3 = noexcept(f3(10));
std::cout << "b3 = " << b3 << "\n" ;// true,c++11后,false,c++11
bool b4 = noexcept(f4<int>());
std::cout << "b4 = " << b4 << "\n" ;// true
A1 a1(10);
// a1.f(1); //error,函数未定义
decltype(a1.f(1)) *p; //OK, f 不求值调用,但需要 noexcept 说明
// a1.pf3 = fa3; //error,指向未定义函数
// a1.pf3 = fa4; //error,要求收窄
a1.pf3 = fa5; //OK
// a1.pf4 = fa3; //error,指向未定义函数
a1.pf4 = fa4; //OK
a1.pf4 = fa5; //OK
//
a1.g1();
a1.g2();
}
//为保证同一类型的不同对象地址始终有别,要求任何对象或成员子对象的大小至少为 1,即使该类型是空的类类型
struct Base {}; // 空类,占用1字节
//如果空成员子对象使用属性 [[no_unique_address]],那么允许像空基类一样优化掉它们。
struct Base1 {
[[no_unique_address]] Base c1; //主动优化,空成员c1被优化掉
Base c2; // Base,占用 1 字节,后随对 i 的填充
int i; // int成员占用 sizeof(int) 字节
};
//基类从对象布局中被优化掉
struct Derived : Base { }; //空基类被优化
//如果首个非静态数据成员的类型与一个空基类的类型相同或者由该空基类派生,
//那么禁用空基类优化,因为要求两个同类型基类子对象在最终派生类型的对象表示中必须拥有不同地址。
struct Derived1 : Base { //空基类被优化
int i; // int成员占用 sizeof(int) 字节
};
struct Derived2 : Base {// 不应用空基优化,基类占用1字节
Base c; //Base 成员占用1字节,后随2个填充字节以满足 int 的对齐要求
int i;
};
struct Derived3 : Base {//不应用空基类优化,基类占用至少 1 字节加填充
Derived1 c; // Derived1成员占用 sizeof(int) 字节
int i; // int成员占用 sizeof(int) 字节
};
struct Derived4 : Base {//空基类被优化
int i; // int成员占用 sizeof(int) 字节
Base c; //Base 成员占用1字节,后随3个填充字节以满足 int 的对齐要求
};
struct Empty {}; // 空类
struct X {
int i;
Empty e; //Empty成员占用1字节,后随3个填充字节以满足 int 的对齐要求
};
struct Y {
int i;
[[no_unique_address]] Empty e;//主动优化,空成员e被优化掉
};
struct Z {
char c;
//e1 与 e2 不能共享同一地址,因为它们拥有相同类型,其中一者可以与 c 共享地址
[[no_unique_address]] Empty e1, e2;//主动优化,空成员e1、e2其中一个被优化掉
};
struct W {
char c[2];
//e1 与 e2 不能拥有同一地址,但它们之一能与 c[0] 共享,而另一者与 c[1] 共享
[[no_unique_address]] Empty e1, e2;//主动优化,空成员e1、e2被优化掉
};
void empty_class_test(void)
{
Empty b1, b2; //构造
b1 = b2; //复制赋值
b1 = std::move(b2);//移动赋值
Empty *pb1=new Empty(), *pb2 = nullptr; //构造
pb2 = pb1; //
pb2 = &b1; //
*pb2 = b2; //复制赋值
pb2 = nullptr;
delete pb1; //析构
pb1 = nullptr;
// 任何空类类型的对象大小至少为 1
std::cout <<"sizeof(Empty) = "<< sizeof(Empty)<<"\n"; //1
// 任何空类类型的对象大小至少为 1
std::cout <<"sizeof(Base) = "<< sizeof(Base)<<"\n";//1
// 应用空基类优化
std::cout <<"sizeof(Base1) = "<< sizeof(Base1) <<"\n"; //8
std::cout <<"sizeof(Derived) = "<< sizeof(Derived) <<"\n"; //1
std::cout <<"sizeof(Derived1) = "<< sizeof(Derived1) <<"\n";//4
std::cout <<"sizeof(Derived2) = "<< sizeof(Derived2) <<"\n";//8
std::cout <<"sizeof(Derived3) = "<< sizeof(Derived3) <<"\n";//12
std::cout <<"sizeof(Derived4) = "<< sizeof(Derived4) <<"\n";//8
}
void no_unique_address_test(void)
{
// 任何空类类型对象的大小至少为 1
std::cout <<"sizeof(Empty) = "<< sizeof(Empty) <<"\n"; //1
// 至少需要多一个字节以给 e 唯一地址
std::cout <<"sizeof(X) = "<< sizeof(X) <<"\n"; //8
// 优化掉空成员
std::cout <<"sizeof(Y) = "<< sizeof(Y) <<"\n";//4
// e1 与 e2 不能共享同一地址,因为它们拥有相同类型,尽管它们标记有 [[no_unique_address]]。
// 然而,其中一者可以与 c 共享地址。
std::cout <<"sizeof(Z) = "<< sizeof(Z) <<"\n";//2
// e1 与 e2 不能拥有同一地址,但它们之一能与 c[0] 共享,而另一者与 c[1] 共享
std::cout <<"sizeof(W) = "<< sizeof(W) <<"\n";//3
};
struct A_common
{
A_common(int) { std::cout <<"A_common(int)\n";} // 转换构造函数
A_common(int, int) { std::cout <<"A_common(int,int)\n";} // 转换构造函数(C++11)
operator bool() const { std::cout <<"A_common bool()\n";return true; }
};
//explicit 说明符只能在类定义之内的构造函数或转换函数 (C++11 起)的 声明说明符序列中出现。
struct B_explicit
{
explicit B_explicit(int) { std::cout <<"B_explicit(int)\n";}
explicit B_explicit(int, int) { std::cout <<"B_explicit(int,int)\n";}
explicit operator bool() const { std::cout <<"B_explicit bool()\n";return true; }
};
constexpr bool cexpr1 = false;
constexpr bool cexpr2 = true;
class C_explicit {
public:
explicit(cexpr1) C_explicit(int) {}; //c++20
};
class D_explicit {
public:
explicit(cexpr2) D_explicit(int) {}; //c++20
};
void explicit_test(void)
{
A_common a1 = 1; // OK:复制初始化选择 A_common::A_common(int)
A_common a2(2); // OK:直接初始化选择 A_common::A_common(int)
A_common a3 {4, 5}; // OK:直接列表初始化选择 A_common::A_common(int, int)
A_common a4 = {4, 5}; // OK:复制列表初始化选择 A_common::A_common(int, int)
A_common a5 = (A_common)1; // OK:显式转型进行 static_cast
if (a1) ; // OK:A_common::operator bool()
bool na1 = a1; // OK:复制初始化选择 A_common::operator bool()
bool na2 = static_cast<bool>(a1); // OK:static_cast 进行直接初始化
// B_explicit b1 = 1; // 错误:复制初始化不考虑 B_explicit::B_explicit(int)
B_explicit b2(2); // OK:直接初始化选择 B_explicit::B_explicit(int)
B_explicit b3 {4, 5}; // OK:直接列表初始化选择 B_explicit::B_explicit(int, int)
// B_explicit b4 = {4, 5}; // 错误:复制列表初始化不考虑 B_explicit::B_explicit(int,int)
B_explicit b5 = (B_explicit)1; // OK:显式转型进行 static_cast
if (b2) ; // OK:B_explicit::operator bool()
// bool nb1 = b2; // 错误:复制初始化不考虑 B_explicit::operator bool()
bool nb2 = static_cast<bool>(b2); // OK:static_cast 进行直接初始化
//
C_explicit ec1=1; //OK
C_explicit ec2(1);
// D_explicit ec3=1; //error
D_explicit ec4(1);
}
#include <stdexcept>
// C++11 constexpr 函数使用递归而非迭代
// C++14 constexpr 函数可使用局部变量和循环
constexpr int factorial(int n)
{
return n <= 1 ? 1 : (n * factorial(n - 1));
}
// 输出要求编译时常量的函数,用于测试
template<int n>
class constN
{
public:
constN() { std::cout << n << '\n'; }
};
// 字面类
class conststr {
const char* p;
std::size_t sz;
public:
template<std::size_t N>
constexpr conststr(const char(&a)[N]): p(a), sz(N - 1) {}
// constexpr 函数通过抛异常来提示错误
// C++11 中,它们必须用条件运算符 ?: 来这么做
constexpr char operator[](std::size_t n) const
{
return n < sz ? p[n] : throw std::out_of_range("");
}
constexpr std::size_t size() const { return sz; }
};
// C++11 constexpr 函数必须把一切放在单条 return 语句中
// (C++14 无此要求)
constexpr std::size_t countlower(conststr s, std::size_t n = 0,
std::size_t c = 0)
{
return n == s.size() ? c :
'a' <= s[n] && s[n] <= 'z' ? countlower(s, n + 1, c + 1) :
countlower(s, n + 1, c);
};
constexpr int sqr1(int arg){
return arg*arg;
}
const int sqr2(int arg){
return arg*arg;
}
#include <array>
#include <coroutine>
// struct task {
// struct promise_type {
// void return_void() {} //co_return调用
// void unhandled_exception() {
// std::cout << "task::promise_type.unhandled_exception \n";
// }
// };
// using Handle = std::coroutine_handle<promise_type>;//协程句柄
// explicit task(Handle coroutine) : m_coroutine{coroutine} {} //get_return_object时调用
// task() = default;
// ~task() {
// std::cout << "~task \n";
// if (m_coroutine) //自行销毁
// {
// m_coroutine.destroy();
// }
// }
// // task(const task&) = delete;
// // task& operator=(const task&) = delete;
// Handle m_coroutine; //协程句柄
// };
class Test1
{
private:
/* data */
char* pc;
public:
Test1(/* args */);
~Test1();
};
Test1::Test1(/* args */)
{
pc = new char[10];
}
Test1::~Test1()
{
delete[] pc; pc = nullptr;
}
class constexprTest : virtual Empty //虚继承影响constexpr的构造和析构
{
public:
// constexpr constexprTest(){}; //error: 'class constexprTest' has virtual base classes
// constexpr ~constexprTest(){}; //c++20前,析构函数不能是 constexpr
// constexpr virtual void f(){}; //error,在此模式下,函数不能同时为 constexpr 和 virtual
// constexpr task coroutine_func(int n = 0) //error: 'co_return' cannot be used in a 'constexpr' function
// {
// co_return;
// }
constexpr Empty f()
{
return Empty();
}
// constexpr Test1 g()//error: invalid return type 'Test1' of 'constexpr' function 'constexpr Test1 constexprTest::g()'
// {
// return Test1();
// }
constexpr void g1(const Test1& obj){};
// constexpr void g3(){
// try //error 语句不能出现在 constexpr 函数中
// {
// /* code */
// }
// catch(const std::exception& e)
// {
// std::cerr << e.what() << '\n';
// }
// }
};
void compile_test(void)
{
int number = 10;
// constN<number> cnumber; //error
constexpr int num = 10;
constN<num> cnum; // OK,在编译时计算
const int numc = 10;
constN<numc> cnumc; //OK
//
std::array<int,sqr1(3)> a1;
// std::array<int,sqr2(3)> a2;//error: call to non-'constexpr' function 'const int sqr2(int)'
//
// constexpr int num1; //error: uninitialized 'const num1' [-fpermissive]
// num1 = 100; //error: assignment of read-only variable 'num1'
constexpr int num2 = 2*num;
constexpr int num3 = factorial(3);
std::cout << "4! = " ;
constN<factorial(4)> out1; // 在编译时计算
volatile int k = 8; // 使用 volatile 防止优化
std::cout << k << "! = " << factorial(k) << '\n'; // 运行时计算
std::cout << "the number of lowercase letters in \"Hello, world!\" is ";
constN<countlower("Hello, world!")> out2; // 隐式转换到常量字符串
};
template<typename T>
class Base3
{
protected:
int var;
};
template<typename T>
class Derived5 : public Base3<T>
{
public:
Derived5()
{
// var = 1; // 错误: 'var' 未在此作用域中声明
this->var = 1; // OK
}
};
class T
{
int x;
void foo()
{
x = 6; // 等同于 this->x = 6;
this->x = 5; // 显式使用 this->
}
void foo() const
{
// x = 7; // 错误:*this 是常量
}
void foo(int x) // 形参 x 遮蔽拥有相同名字的成员
{
this->x = x; // 无限定的 x 代表形参
// 需要用‘this->’消歧义
}
int y;
T(int x) : x(x), // 用形参 x 初始化成员 x
y(this->x) // 用成员 x 初始化成员 y
{}
T& operator= ( const T& b )
{
x = b.x;
return *this; // 许多重载运算符都返回 *this
}
};
class Outer {
// int a[sizeof(*this)]; // 错误:不在成员函数中
unsigned int sz = sizeof(*this); // OK:在默认成员初始化器中
void f() {
int b[sizeof(*this)]; // OK
struct Inner {
// int c[sizeof(*this)]; // 错误:不在 Inner 的成员函数中
void f()
{
int c[sizeof(*this)];// OK
};
};
}
};