5 工具
约 5836 字大约 19 分钟
2025-06-22
静态是指编译期的 动态指运行期,等拥有了内存之后才产生的
运行时断言
在静态断言出现以前,我们使用的是运行时断言,只有程序运行起来之后才有可能触发它。 通常情况下运行时断言只会在Debug模式下使用,因为断言的行为比较粗暴,它会直接显示错误信息并终止程序。 断言不能 代替程序中的错误检查,它只应该出现在需要表达式返回true的位置,例如:算术表达式的除数 不能为0,分配内存的大小必须大于0等。相反,如果表达式中涉及外部输入,则不应该依赖断言,例如客户输入、服务端返回等
对一个表达式的判断,当表达式为假时就输出诊断消息并调用abort()函数中止程序。 语法:
assert(bool_constexpr );
需要包含头文件< assert.h>
每个断言只能检测一个条件。因为条件过多,当出现错误时,无法判断是哪个条件出错。 不能在断言中放入改变源程序数值的语句,例如assert(++i == 3); 因为频繁调用断言会影响程序性能,因此有时需要禁用断言。只需要在头文件里添加NDEBUG的宏定义。
#define NDEBUG
虽然运行时断言可以满足一部分需求,但是它有一个缺点就是必须让程序运行到断言代码的位置才会触发断言。如果想在模板实例化的时候对模板实参进行约束,这种断言是无法办到的。 可以通过其他特性来模拟
#define STATIC_ASSERT_CONCAT_IMP(x, y) x ## y
#define STATIC_ASSERT_CONCAT(x, y) \
STATIC_ASSERT_CONCAT_IMP(x, y)
// 方案1
#define STATIC_ASSERT(expr) \
do { \
char STATIC_ASSERT_CONCAT( \
static_assert_var, __COUNTER__) \
[(expr) != 0 ? 1 : -1]; \
} while (0)
template<bool>
struct static_assert_st;
template<>
struct static_assert_st<true> {
};
// 方案2
#define STATIC_ASSERT2(expr) \
static_assert_st<(expr) != 0>()
// 方案3
#define STATIC_ASSERT3(expr) \
static_assert_st<(expr) != 0> \
STATIC_ASSERT_CONCAT( \
static_assert_var, __COUNTER__)
方案1,利用的技巧是数组的大小不能为负值,当expr表达式返回结果为false的时 候,条件表达式求值为−1,这样就导致数组大小为−1,自然就会引发编译失败。 方案2和方案3则是利用了C++模板特化的特性,当模板实参为true的时候,编译器能找到特化版本的定义。但当模板参数为false的时候,编译器无法找到相应的特化定义,从而编译失败。 方案2和方案3的区别在于,方案2会构造临时对象,这让它无法出现在类和结构体的定义当中。而方案3则声明了一个变量,可以出现在结构体和类的定义中,但是它最大的问题是会改变结构体和类的内存布局。
静态断言
用于在程序编译阶段评估常量表达式并对返回 false的表达式断言
使用static_assert需要传入两个实参:常量表达式和诊断消息字符串。第一个实参必须是常量表达式,因为编译器无法计算运行时才能确定结果的表达式
class A {
};
class B : public A {
};
class C {
};
template<class T>
class E {
static_assert(is_base_of<A, T>::value, "T is not base of A");
};
int main(int argc, char *argv[]) {
static_assert(argc > 0, "argc > 0"); // 使用错误,argc>0不是常量表达式
E<C> x; // 使用正确,但由于A不是C的基类,所以触发断言
static_assert(sizeof(int) >= 4, // 使用正确,表达式返回真,不会触发失败断言"sizeof(int) >= 4");
E<B> y; // 使用正确,A是B的基类,不会触发失败断言
}
argc > 0依赖于用户输入的参数,显然不是一个常量表达式。在这种情况下,编译器会报错,符合上面的第5条要求。类模板E对static_ assert的使用是正确的,根据第1条和第4条要求,static_assert可以在类定义里使用并且不会改变类的内部状态。只不过在实例化类模板E< C>的时候,因为A不是C的基类,所以会触发静态断言,导致编译中断
单参数static_assert
在大多数情况下使用static_assert的时候输入的诊断信 息字符串就是常量表达式本身,所以让常量表达式作为诊断信息字符串参数的默认值是非常理想的。为了达到这个目的,我们可以定义一个宏
#define LAZY_STATIC_ASSERT(B) static_assert(B, #B)
可能是该需求比较普遍的原因,2014年2月C++标准委员会就提出升级static_ assert的想 法,希望让其支持单参数版本,即常量表达式,而断言输出的诊断信息为常量表达式本身。这个 观点提出后得到了大多数人的认同,但是由于2014年2月C++14标准已经发布了,因此该特性不 得不顺延到C++17标准中。在支持C++17标准的环境中,我们可以忽略第二个参数
class A {
};
class B : public A {
};
class C {
};
template<class T>
class E {
static_assert(is_base_of<A, T>::value);
};
int main(int argc, char *argv[]) {
E<C> x; // 使用正确,但由于A不是C的基类,会触发失败断言
static_assert(sizeof(int) < 4); // 使用正确,但表达式返回false,会触发失败断言
}
结构化绑定
在C++11标准中引入了元组的概念,通过元组C++能返回多个值 需要包含头文件< tuple>
tuple<int,int> return_multiple_values(){
return make_tuple(11, 7);
}
int main(){
int x = 0, y = 0;
tie(x, y) = return_multiple_values();
cout << x << ',' << y << endl;
}
这段代码和Python完成了同样的工作,但代码却要麻烦许多。其中一个原因是 C++11必须指定return_multiple_values函数的返回值类型,另外,在调用return_multiple_values 函数前还需要声明变量x和y,并且使用函数模板stdtuple上。 解决第一个问题可以使用auto关键字声明函数返回类型 解决第二个问题就需要使用结构化绑定
结构化绑定是指将一个或者多个名称绑定到初始化对象中的一个或者多个子对象(或者元素) 上,相当于给初始化对象的子对象(或者元素)起了别名,别名不同于引用
auto return_multiple_values() {
return make_tuple(11, 7);
}
int main() {
auto [x, y] = return_multiple_values();
cout << x << ',' << y <<endl;
}
auto[x,y] = return_multiple_values()是一个典型的结构化绑定声明,其中auto是类型占位符,[x, y]是绑定标识符列表,其中x和y是用于绑定的名称,绑定的目标是函数 return_multiple_values()返回结果副本的子对象或者元素。 结构化绑定的目标不必是一个函数的返回结果,等号的右边可以是任意一个合理的表达式
class A{
public:
int a=6;
string s="Aeolian";
};
int main() {
A a;
auto [x, y] = a;
cout << x << ',' << y <<endl;
}
结构化绑定能够直接绑定到结构体上。也可以将其运用到基于范围的for循环
struct BindTest {
int a = 42;
string s = "Aeolian";
};
int main() {
vector<BindTest> bt{{11, "hello"},{7, "c++"},{42, "world"}};
for (const auto &[x, y]: bt) {
cout << x << ',' << y << endl;
}
}
结构化绑定的本质
在结构化 绑定中编译器会根据限定符生成一个等号右边对象的匿名副本,而绑定的对象正是这个副本而非原对象本身。 这里的别名真的是单纯的别名,别名的类型和绑定目标对象的子对象类型相同,而引用类型本身就是一种和非引用类型不同的类型。 编译器生成的副本是使用const auto修饰的,也就是说副本是const还是volatile完全依赖于auto的限定符 如果在试图使用x和y去修改bt的数据成员是无法成功的,因为一方面x和y都是常量类 型;另一方面即使x和y是非常量类型,改变的x和y只会影响匿名对象而非bt本身。
可以使用引用的结构化绑定修改bt成员变量
BindTest bt;
auto&[x, y] = bt;
cout << &bt.a << ',' << &x << endl;
cout << &bt.s << ',' << &y << endl;
x = 11;
cout << bt.a << endl;
bt.s = "hi structured binding";
cout << y << endl;
虽然只是将const auto修改为auto&,但是已经能达到让bt数据成员和x、y相互修改的目的了
如果结构化绑定声明为const auto&[x, y] = bt,那么x = 11会编译失败,因为x绑定的对象是一个常量引用,而bt.s = "hi structured binding"却能成功修改y的值,因为bt本身不存在常量问题。
使用结构化绑定无法忽略对象的子对象或者元素
auto t = make_tuple(42, "hello world");
auto [x] = t;//error
结构化绑定的3种类型
结构化绑定可以作用于3种类型,包括原生数组、结构体和类对象、元组和类元组的对象
原生数组
绑定到原生 数组即将标识符列表中的别名一一绑定到原生数组对应的元素上。所需条件仅仅是要求别名的数量与数组元素的个数一致
int a[3]{ 1, 3, 5 };
auto[x, y, z] = a;
cout<< x << ','<< y << ','<< z << endl;
绑定到原生数组需要小心数组的退化,因为在绑定的过程中编译器必须知道原生数组的元素个数,一旦数组退化为指针,就将失去这个属性。
结构体和类对象
将标识符列表中的别名分别绑定到结构体和类的非静态成员变量上的限制条件要比原生数组复杂得多 首先,类或者结构体中的非静态数据成员个数必须和标识符列表中的别名的个数相同; 其次,这些数据成员必须是公有的 这些数据成员必须是在同一个类或者基类中; 最后,绑定的类和结构体中不能存在匿名联合体:
class BindBase1 {
public:
int a = 42;
double b = 11.7;
};
class BindTest1 : public BindBase1 {
};
class BindBase2 {
};
class BindTest2 : public BindBase2 {
public:
int a = 42;
double b = 11.7;
};
class BindBase3 {
public:
int a = 42;
};
class BindTest3 : public BindBase3 {
public:
double b = 11.7;
};
int main() {
BindTest1 bt1;
BindTest2 bt2;
BindTest3 bt3;
auto [x1, y1] = bt1; // 编译成功
auto [x2, y2] = bt2; // 编译成功
auto [x3, y3] = bt3; // 编译错误
}
auto[x1, y1] = bt1和auto[x2, y2] = bt2可以顺利地编译,因为类 BindTest1和BindTest2的非静态数据成员要么全部在派生类中定义,要么全部在基类中定义。 BindTest3却不同,其中成员变量a的定义在基类,成员变量b的定义在派生类,这一点违反了绑定 结构体的限制条件,所以auto[x3, y3] = bt3会导致编译错误。最后需要注意的是,类和结构体中 不能出现匿名的联合体,而对于命名的联合体则没有限制。
元组和类元组对象
绑定到元组就是将标识符列表中的别名分别绑定到元组对象的各个元素。
绑定元组和类元组有一系列抽象的条件: 对于元组或者类元组类型T。
- 需要满足stdvalue是一个符合语法的表达式,并且该表达式获得的整数 值与标识符列表中的别名个数相同。
- 类型T还需要保证stdtype也是一个符合语法的表达式,其中i是小于stdvalue的整数,表达式代表了类型T中第i个元素的类型。
- 类型T必须存在合法的成员函数模板get_()或者函数模板get_(t),其中i是小于 stdvalue的整数,t是类型T的实例,get_()和get_(t)返回的是实例t中第i个元素的值。
这些条件并没有明确规定结构化绑定的类型一定是元组,任何具有上述条件特征的类型都可以成为绑定的目标。另外,获取这些条件特征的代价 也并不高,只需要为目标类型提供stdtuple_element以及get的特化或者偏特化版本即可。 标准库中除了元组本身能够作为绑定目标以外,stdarray也能作为结构化绑定的目标,其原因就是它们是满足上述条件的类元组。
对于pair
map<int,string> id2str{ {1, "hello"},
{3, "Structured"},
{5, "bindings"}
};
for (const auto& elem : id2str) {
cout << elem.first<< ',' << elem.second << endl;
}
上面这段代码是一个基于范围的for循环遍历stdpair类型,要在循环体中输出key和value的值就需要访问成员变量first和second。这个例 子中使用基于范围的for循环已经比使用迭代器遍历stdpair的成员变量first和second绑定到别名以保证代码阅读起来更加清晰
map<int,string> id2str{ {1, "hello"},
{3, "Structured"},
{5, "bindings"}
};
for (const auto&[id, str]:id2str) {
cout << id<< ',' << str << endl;
}
实现一个类元组类型
BindTest3,我们知道由于它的数据成员分散在派生类和基类之中,因此无法使用结构化绑定。下面将通过让其满足类元组的条件,从而达到支持结构化绑定 的目的
class BindBase3 {
public:
int a = 42;
};
class BindTest3 : public BindBase3 {
public:
double b = 11.7;
};
namespace std {
template<>
struct tuple_size<BindTest3> {
static constexpr size_t value = 2;
};
template<>
struct tuple_element<0, BindTest3> {
using type = int;
};
template<>
struct tuple_element<1, BindTest3> {
using type = double;
};
}
template<std::size_t Idx>
auto &get(BindTest3 &bt) = delete;
template<>
auto &get<0>(BindTest3 &bt) { return bt.a; }
template<>
auto &get<1>(BindTest3 &bt) { return bt.b; }
int main() {
BindTest3 bt3;
auto &[x3, y3] = bt3;
x3 = 78;
std::cout << bt3.a << std::endl;
}
我们为BindTest3实现了3种特性以满足类元组的限制条件。首先实现的
template<>
struct tuple_size {
static constexpr size_t value = 2;
};
它的作用是告诉编译器将要绑定的子对象和元素的个数,这里通过特化让 tuple_size::value的值为2,也就是存在两个子对象。然后需要明确的是每个子对象和 元素的类型:
template<>
struct tuple_element<0, BindTest3> {
using type = int;
};
template<>
struct tuple_element<1, BindTest3> {
using type = double;
};
这里同样通过特化的方法指定了两个子对象的具体类型。最后需要实现的是get函数,注 意,get函数的实现有两种方式,一种需要给BindTest3添加成员函数;另一种则不需要,我们通常 会选择不破坏原有代码的方案,所以这里先展示后者:
template<std::size_t Idx>
auto &get(BindTest3 &bt) = delete;
template<>
auto &get<0>(BindTest3 &bt) { return bt.a; }
template<>
auto &get<1>(BindTest3 &bt) { return bt.b; }
可以看到函数模板get也特化出了两个函数实例,它们分别返回bt.a和bt.b的引用。之所以这 里需要返回引用,是因为我希望结构化绑定的别名能够修改BindTest3的实例,如果需要的是一个 只读的结构化绑定,则这里可以不必返回引用。最后template auto& get(BindTest3 &bt) = delete可以明确地告知编译器不要生成除了特化版本以外的函数实例以防止 get函数模板被滥用。 正如上文强调的,我不推荐实现成员函数版本的get函数,因为这需要修改原有的代码。但是 当我们重新编写一个类,并且希望它支持结构化绑定的时候,也不妨尝试实现几个get成员函数:
class BindBase3 {
public:
int a = 42;
};
class BindTest3 : public BindBase3 {
public:
double b = 11.7;
template<std::size_t Idx>
auto &get() = delete;
};
template<>
auto &BindTest3::get<0>() { return a; }
template<>
auto &BindTest3::get<1>() { return b; }
namespace std {
template<>
struct tuple_size<BindTest3> {
static constexpr size_t value = 2;
};
template<>
struct tuple_element<0, BindTest3> {
using type = int;
};
template<>
struct tuple_element<1, BindTest3> {
using type = double;
};
}
int main() {
BindTest3 bt3;
auto &[x3, y3] = bt3;
x3 = 78;
cout << bt3.a << endl;
}
这段代码和第一份实现代码基本相同,我们只需要把精力集中到get成员函数的部分
class BindTest3 : public BindBase3 {
public:
double b = 11.7;
template<std::size_t Idx> auto& get() = delete;
};
template<> auto& BindTest3::get<0>() { return a; }
template<> auto& BindTest3::get<1>() { return b; }
这段代码中get成员函数的优势显而易见,成员函数不需要传递任何参数。另外,特化版本的 函数get<0>和get<1>可以直接返回a和b,这显得格外简洁。读者不妨自己编译运行一下这两段代码,其输出结果应该都是78,修改bt.a成功。
绑定的访问权限问题
当在结构体或者类中使用结构化绑定的时候,需要有公开的访问权限,否则会 导致编译失败。这条限制乍看是合理的,但是仔细想来却引入了一个相同条件下代码表现不一致的问题
struct A {
friend void foo();
private:
int i;
};
void foo() {
A a{};
auto x = a.i; // 编译成功
auto [y] = a; // 编译失败
}
在上面这段代码中,foo是结构体A的友元函数,它可以访问A的私有成员i。但是,结构化绑 定却失败了,这就明显不合理了。同样的问题还有访问自身成员的时候:
class C {
int i;
void foo(const C& other) {
auto [x] = other; // 编译失败
}
};
为了解决这类问题,C++20标准规定结构化绑定的限制不再强调必须为公开数据成员,编译 器会根据当前操作的上下文来判断是否允许结构化绑定。幸运的是,虽然标准是2018年提出修改 的,但在实验的3种编译器上,无论是C++17还是C++20标准,以上代码都可以顺利地通过编译。
可调用对象包装器、绑定器
可调用对象包装器、绑定器
可调用对象
函数指针、具有operator()的类对象(仿函数)、可被转换为函数指针的类对象、类成员的函数指针或类成员指针
将类对象转换为函数指针
typedef void(*funcptr)(int,string);
class Test{
public:
int a=10;
void hello(int a,string s){cout<<"hello"<<a<<s<<endl;}
static void world(int a,string s){cout<<"world"<<a<<s<<endl;}
operator funcptr(){
return hello; //error
return world; //ok
}
};
类成员在实例化类对象前是不存在的,但静态成员是存在的(静态方法是属于类的)
类的函数指针
如果想要创建能调用类的非静态成员的指针就需要创建类的函数指针了 对于以上案例只需要在函数指针声明中加上类的作用域即可
Test t;
using fptr= void(Test::*)(int,string);
fptr f1=&Test::hello;
(t.*f1)(16,"jerry");//相等(&t->*f1)(16,"jerry");
类的成员指针
typedef int Test::*ptr1;
ptr1 p=&Test::a;
cout<<t.*p<<endl;
可调用对象包装器
需要包含头文件< functional > std::function是可调用对象的包装器,他是一个类模板,可以容纳除类成员(函数)指针之外所有可调用对象通过它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们 语法
function<返回值类型(参数类型列表)> diy_name = 可调用对象;
声明和调用
typedef void(*funcptr)(int,string);
class Test{
public:
int a=10;
void hello(int a,string s){cout<<"类的成员函数"<<a<<s<<endl;}
static void world(int a,string s){cout<<"类的静态函数"<<a<<s<<endl;}
void operator()(string s){cout<<"仿函数"<<s<<endl;}
operator funcptr(){
cout<<"转换成函数指针的类对象"<<endl;
return world;
}
};
void test(int a,string s){cout<<"普通函数"<<a<<s<<endl;}
//包装普通函数
function<void(int,string)> p1=test;
p1(1,"a");
//包装类的静态成员
function<void(int,string)> p2=Test::world;
p2(2,"b");
//包装仿函数
Test t;
function<void(string)> p3=t;//仿函数可调用对象就是t本身
p3("c");
//包装转换为函数指针的类对象
function<void(int,string)> p4=t;
p4(4,"d");
通过可调用对象包装器将他们打包之后就能得到统一的类型,基于这个类型我们可以像函数一样调用,也可以作为函数的参数传递 实际应用
class A{
private:
function<void(int,string)>callback;
public:
A(const function<void(int,string)> & f):callback(f){}
void notify(int id,string name){
callback(id,name);
}
};
A a(test);
a.notify(1,"as");
A b(Test::world);
b.notify(2,"df");
A c(t);
c.notify(3,"cx");
可调用对象绑定器
stdfunction进行保存,并延迟调用到任何我们需要的对象 作用 1、将可调用对象与其参数一起绑定成一个仿函数 2、将多元(参数个数为m,n>1)可调用对象转换为一元或者(n+1)元可调用对向,即只绑定部分参数 语法
//绑定非类成员函数/变量
auto f=std::bind(可调用对象地址,绑定的参数/占位符);
//绑定类成员函数/变量
auto f=std::bind(类函数/成员地址,类实例对象地址,绑定的参数/占位符);
静态函数不属于类对象,因此应当使用第一种方式 绑定器返回的是一个仿函数类型,得到的返回值可以直接赋值给一个std::function,在使用的时候我们不需要关心绑定器的放回值类型,使用auto进行了自动类型推导即可
占位符
placeholders_ 1、placeholders_ 3、placeholders_ 5等 占位符在调用时就转换为后面小括号里传入的参数 占位符后面的数是多少就表示找后面小括号里第几个参数
void test(int a,int s){cout<<"普通函数"<<a<<s<<endl;}
bind(test,1,placeholders::_1)(3); //test的x参数传入1,y参数为后面括号第一个参数
bind(test,placeholders::_1,2)(3); //test的y参数传入2,x参数为后面括号第一个参数
bind(test,placeholders::_1,placeholders::_2)(3,4); //test的y参数为后面括号第一个参数,x参数为后面括号第二个参数
bind(test,placeholders::_2,placeholders::_1)(3,4);//test的y参数为后面括号第二个参数,x参数为后面括号第一个参数
bind(test,2,placeholders::_1)(3,4);//使用绑定的数值而使用参入的数值
绑定器函数绑定类成员函数(变量)
//成员函数绑定
Test t;
auto f1=bind(&Test::hello,&t,20,placeholders::_1);
function<void(int,string)> f11=bind(&Test::hello,&t,20,placeholders::_1);
f1("2077");
//成员变量
auto f2= bind(&Test::a,&t);
function<int()> f22=bind(&Test::a,&t);//只读
function<int&()> f23=bind(&Test::a,&t);//加上引用可修改
cout<<f2()<<endl;
f2()=6;//可修改
cout<<f2()<<endl;
贡献者
版权所有
版权归属:PinkDopeyBug