diff --git a/basic_content/abstract/README.md b/basic_content/abstract/README.md index 15f3ae6..9681bb1 100644 --- a/basic_content/abstract/README.md +++ b/basic_content/abstract/README.md @@ -1,10 +1,12 @@ -# 纯虚函数和抽象类 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/abstract/) 同步生成,网站版本含多语言与更新排版。 + +# C++ 纯虚函数与抽象类 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 1.纯虚函数与抽象类 @@ -23,7 +25,7 @@ public: 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象,抽象类的指针和引用->由抽象类派生出来的类的对象! -> 代码样例:[abstract_base.h](./abstract_base.h)、[pure_virtual.cpp](./pure_virtual.cpp) +> 代码样例:[abstract_base.h](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/abstract/abstract_base.h)、[pure_virtual.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/abstract/pure_virtual.cpp) ## 2.实现抽象类 @@ -45,11 +47,11 @@ public: }; ``` -> 代码样例:[abstract.cpp](./abstract.cpp) +> 代码样例:[abstract.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/abstract/abstract.cpp) ## 3.重要点 -- [纯虚函数使一个类变成抽象类](./interesting_facts1.cpp) +- [纯虚函数使一个类变成抽象类](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/abstract/interesting_facts1.cpp) ```cpp // 抽象类至少包含一个纯虚函数 class Base{ @@ -62,7 +64,7 @@ private: }; ``` -- [抽象类类型的指针和引用](./interesting_facts2.cpp) +- [抽象类类型的指针和引用](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/abstract/interesting_facts2.cpp) ```cpp class Derived : public Base { public: @@ -81,7 +83,7 @@ int main(void) } ``` -- [如果我们不在派生类中覆盖纯虚函数,那么派生类也会变成抽象类](./interesting_facts3.cpp) +- [如果我们不在派生类中覆盖纯虚函数,那么派生类也会变成抽象类](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/abstract/interesting_facts3.cpp) ```cpp // Derived为抽象类 class Derived: public Base @@ -91,7 +93,7 @@ public: }; ``` -- [抽象类可以有构造函数](./interesting_facts4.cpp) +- [抽象类可以有构造函数](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/abstract/interesting_facts4.cpp) ```cpp // 抽象类 class Base { @@ -111,7 +113,7 @@ public: }; ``` -- [构造函数不能是虚函数,而析构函数可以是虚析构函数](./interesting_facts5.cpp) +- [构造函数不能是虚函数,而析构函数可以是虚析构函数](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/abstract/interesting_facts5.cpp) ```cpp // 抽象类 class Base { @@ -137,4 +139,4 @@ public: 抽象类由派生类继承实现! -> 代码样例:[derived_full.cpp](./derived_full.cpp) \ No newline at end of file +> 代码样例:[derived_full.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/abstract/derived_full.cpp) \ No newline at end of file diff --git a/basic_content/assert/README.md b/basic_content/assert/README.md index 8699eff..af67be6 100644 --- a/basic_content/assert/README.md +++ b/basic_content/assert/README.md @@ -1,10 +1,12 @@ -# assert那些事 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/assert/) 同步生成,网站版本含多语言与更新排版。 + +# C++ assert 断言 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) @@ -20,7 +22,7 @@ assert 宏的原型定义在 (C)、(C++)中。其作 void assert(int expression); ``` -> 代码样例:[assert.c](./assert.c) +> 代码样例:[assert.c](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/assert/assert.c) ```c #include #include @@ -58,4 +60,4 @@ assert: assert.c:13: main: Assertion 'x==7' failed. #define NDEBUG // 加上这行,则 assert 不可用 ``` -> 样例代码:[ignore_assert.c](./ignore_assert.c) +> 样例代码:[ignore_assert.c](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/assert/ignore_assert.c) \ No newline at end of file diff --git a/basic_content/bit/README.md b/basic_content/bit/README.md index c5b40d6..c40f56a 100644 --- a/basic_content/bit/README.md +++ b/basic_content/bit/README.md @@ -1,8 +1,12 @@ +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/bit/) 同步生成,网站版本含多语言与更新排版。 + +# C++ 位域 + ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## Bit field 是什么? @@ -108,7 +112,7 @@ struct stuff 这里 stuff.field1 与 stuff.field2 之间有一个 2 Bits 的空隙,stuff.field3 则存储在下一个 unsigned int 中,该结构现在大小为 3 * 32 = 96 Bits。 -学习代码见:[bit.cpp](bit.cpp) +学习代码见:[bit.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/bit/bit.cpp) ## 位域的初始化和位的重映射 diff --git a/basic_content/c_poly/README.md b/basic_content/c_poly/README.md index ed91220..e4e610e 100644 --- a/basic_content/c_poly/README.md +++ b/basic_content/c_poly/README.md @@ -1,10 +1,12 @@ -# C实现C++的面向对象特性 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/c-polymorphism/) 同步生成,网站版本含多语言与更新排版。 + +# C 语言实现 C++ 多态 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 1.C++实现案例 @@ -12,7 +14,7 @@ C++中的多态:在C++中会维护一张虚函数表,根据赋值兼容规则 如果一个父类的指针或者引用调用父类的虚函数则该父类的指针会在自己的虚函数表中查找自己的函数地址,如果该父类对象的指针或者引用指向的是子类的对象,而且该子类已经重写了父类的虚函数,则该指针会调用子类的已经重写的虚函数。 -学习案例代码见:[c++_examp.cpp](./c++_examp.cpp) +学习案例代码见:[c++_examp.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/c_poly/c++_examp.cpp) @@ -38,4 +40,4 @@ C语言中是没有class类这个概念的,但是有struct结构体,我们 -学习案例代码见:[c_examp.c](./c_examp.c) +学习案例代码见:[c_examp.c](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/c_poly/c_examp.c) \ No newline at end of file diff --git a/basic_content/const/README.md b/basic_content/const/README.md index cf1367d..fabc27f 100644 --- a/basic_content/const/README.md +++ b/basic_content/const/README.md @@ -1,8 +1,12 @@ +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/const/) 同步生成,网站版本含多语言与更新排版。 + +# C++ const 常量与修饰 + ## 关于作者 微信公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 1.const含义 @@ -34,8 +38,9 @@ void f(const int i){ + 可以节省空间,避免不必要的内存分配 - + const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像`#define`一样给出的是立即数。 - + const定义的常量在程序运行过程中只有一份拷贝,而`#define`定义的常量在内存中有若干个拷贝。 + + 以整型/枚举类型、常量表达式初始化的 const(即常量表达式)在用法上更接近编译期常量:不取地址、仅使用其值时通常被优化成立即数,与 `#define` 类似;取地址或跨编译单元 odr-use 时才会落到内存,且全程序只有一份。 + + `#define` 展开为字面量后以立即数形式编入指令,指令本身也占内存,多处使用即多处出现(见 [issue #5](https://github.com/Light-City/CPlusPlusThings/issues/5) 的讨论)。 + + 注意:并非所有 const 变量都是常量表达式——只有整数/枚举类型且以常量表达式初始化的才是;其他 const 变量只是 const 限度的变量(如浮点、自定义类型,标准并未规定其可用常量表达式初始化,`constexpr` 才是编译期常量的正确表达)。 ## 3.const对象默认为文件局部变量 @@ -358,7 +363,7 @@ int main() ``` > 编译:bazel run basic_content/const/class_const/first_example:main
-此时报错,上面getCount()方法中调用了一个add方法,而add方法并非const修饰,所以运行报错。也就是说const成员函数只能访问const成员函数。 +此时报错:getCount() 是 const 成员函数,其中的 add() 重载决议只会匹配 const 版本(`int add(int num) const`);把这个 const 版本注释掉后,才会因调用了非 const 的 add() 而报错。即:const 对象只能访问 const 成员函数,非 const 对象可以访问任意成员函数(见 [issue #338](https://github.com/Light-City/CPlusPlusThings/issues/338))。 当调用改为: @@ -410,4 +415,4 @@ static int ap; int Apple::ap=666 ``` -对于此项,c++11不能进行声明并初始化,也就是上述使用方法。 +对于此项,c++11不能进行声明并初始化,也就是上述使用方法。 \ No newline at end of file diff --git a/basic_content/decltype/README.md b/basic_content/decltype/README.md index ac214b3..3ed43fe 100644 --- a/basic_content/decltype/README.md +++ b/basic_content/decltype/README.md @@ -1,10 +1,12 @@ -# decltype那些事 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/decltype/) 同步生成,网站版本含多语言与更新排版。 + +# C++ decltype 类型推导 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 1.基本使用 decltype的语法是: @@ -69,7 +71,7 @@ auto multiply(T x, T y)->decltype(x*y) } ``` -完整代码见:[decltype.cpp](decltype.cpp) +完整代码见:[decltype.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/decltype/decltype.cpp) ## 2.判别规则 @@ -135,4 +137,4 @@ decltype(Func(1)) var13=true;//const bool decltype(i++) var14 = i;//int i++返回右值 ``` -学习参考:https://www.cnblogs.com/QG-whz/p/4952980.html +学习参考:https://www.cnblogs.com/QG-whz/p/4952980.html \ No newline at end of file diff --git a/basic_content/enum/README.md b/basic_content/enum/README.md index 5386c41..9e73de9 100644 --- a/basic_content/enum/README.md +++ b/basic_content/enum/README.md @@ -1,10 +1,12 @@ -# 从初级到高级的enum那些事 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/enum/) 同步生成,网站版本含多语言与更新排版。 + +# C++ enum 枚举类型 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 传统行为 @@ -37,7 +39,7 @@ int main() - 会隐式转换为int - 用来表征枚举变量的实际类型不能明确指定,从而无法支持枚举类型的前向声明。 -具体实现见:[tradition_color.cpp](tradition_color.cpp) +具体实现见:[tradition_color.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/enum/tradition_color.cpp) ## 经典做法 @@ -72,7 +74,7 @@ struct Color1 }; ``` -具体实现见:[classic_practice.cpp](classic_practice.cpp) +具体实现见:[classic_practice.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/enum/classic_practice.cpp) ## C++11 的枚举类 @@ -110,7 +112,7 @@ enum class Color3:char char c3 = static_cast(Color3::RED); ``` -具体实现见:[classic_practice.cpp](classic_practice.cpp) +具体实现见:[classic_practice.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/enum/classic_practice.cpp) ## 类中的枚举类型 @@ -161,4 +163,4 @@ public: 枚举常量不会占用对象的存储空间,它们在编译时被全部求值。 -枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点。 +枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点。 \ No newline at end of file diff --git a/basic_content/explicit/README.md b/basic_content/explicit/README.md index 740b7b0..b66e444 100644 --- a/basic_content/explicit/README.md +++ b/basic_content/explicit/README.md @@ -1,16 +1,18 @@ -# explicit(显式)关键字那些事 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/explicit/) 同步生成,网站版本含多语言与更新排版。 + +# C++ explicit 显式转换 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) - explicit 修饰构造函数时,可以防止隐式转换和复制初始化 - explicit 修饰转换函数时,可以防止隐式转换,但按语境转换除外 -代码参见:[.explicit.cpp](./explicit.cpp) +代码参见:[.explicit.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/explicit/explicit.cpp) 参考链接: -> https://stackoverflow.com/questions/4600295/what-is-the-meaning-of-operator-bool-const +> https://stackoverflow.com/questions/4600295/what-is-the-meaning-of-operator-bool-const \ No newline at end of file diff --git a/basic_content/extern/README.md b/basic_content/extern/README.md index 215d201..92ac950 100644 --- a/basic_content/extern/README.md +++ b/basic_content/extern/README.md @@ -1,10 +1,12 @@ -# extern "C" +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/extern/) 同步生成,网站版本含多语言与更新排版。 + +# C++ extern 链接说明 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 1.C++与C编译区别 @@ -118,11 +120,11 @@ int add(int x,int y); 上述案例源代码见: -- [add.h](extern_c++/add.h) +- [add.h](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/extern/extern_c++/add.h) -- [add.c](extern_c++/add.c) +- [add.c](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/extern/extern_c++/add.c) -- [add.cpp](extern_c++/add.cpp) +- [add.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/extern/extern_c++/add.cpp) ## 2.C中调用C++函数 @@ -166,11 +168,11 @@ gcc add.c add.o -o main 上述案例源代码见: -- [add.h](extern_c/add.h) +- [add.h](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/extern/extern_c/add.h) -- [add.c](extern_c/add.c) +- [add.c](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/extern/extern_c/add.c) -- [add.cpp](extern_c/add.cpp) +- [add.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/extern/extern_c/add.cpp) 综上,总结出使用方法,在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。所以使用extern "C"全部都放在于cpp程序相关文件或其头文件中。 @@ -209,4 +211,4 @@ extern int add(); ``` -不过与C++调用C接口不同,C++确实是能够调用编译好的C函数,而这里C调用C++,不过是把C++代码当成C代码编译后调用而已。也就是说,C并不能直接调用C++库函数。 +不过与C++调用C接口不同,C++确实是能够调用编译好的C函数,而这里C调用C++,不过是把C++代码当成C代码编译后调用而已。也就是说,C并不能直接调用C++库函数。 \ No newline at end of file diff --git a/basic_content/friend/README.md b/basic_content/friend/README.md index 64ba791..7dc198f 100644 --- a/basic_content/friend/README.md +++ b/basic_content/friend/README.md @@ -1,10 +1,12 @@ -# 友元函数与友元类 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/friend/) 同步生成,网站版本含多语言与更新排版。 + +# C++ friend 友元 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 0.概述 @@ -35,7 +37,7 @@ friend <类型><友元函数名>(<参数表>); 注意,友元函数只是一个普通函数,并不是该类的类成员函数,它可以在任何地方调用,友元函数中通过对象名来访问该类的私有或保护成员。 -具体代码见:[friend_func.cpp](friend_func.cpp) +具体代码见:[friend_func.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/friend/friend_func.cpp) ```c++ #include @@ -74,7 +76,7 @@ friend class <友元类名>; 类B是类A的友元,那么类B可以直接访问A的私有成员。 -具体代码见:[friend_class.cpp](friend_class.cpp) +具体代码见:[friend_class.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/friend/friend_class.cpp) ```c++ #include @@ -112,4 +114,4 @@ int main() 假如类B是类A的友元,类C继承于类A,那么友元类B是没办法直接访问类C的私有或保护成员。 - 友元关系没有传递性 -假如类B是类A的友元,类C是类B的友元,那么友元类C是没办法直接访问类A的私有或保护成员,也就是不存在“友元的友元”这种关系。 +假如类B是类A的友元,类C是类B的友元,那么友元类C是没办法直接访问类A的私有或保护成员,也就是不存在“友元的友元”这种关系。 \ No newline at end of file diff --git a/basic_content/inline/README.md b/basic_content/inline/README.md index 772855f..0798a45 100644 --- a/basic_content/inline/README.md +++ b/basic_content/inline/README.md @@ -1,10 +1,12 @@ -# inline那些事 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/inline/) 同步生成,网站版本含多语言与更新排版。 + +# C++ inline 内联函数 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 1.类中内联 @@ -131,7 +133,4 @@ int main() return 0; } -``` - - - +``` \ No newline at end of file diff --git a/basic_content/macro/README.md b/basic_content/macro/README.md index d20b612..7b6ee7a 100644 --- a/basic_content/macro/README.md +++ b/basic_content/macro/README.md @@ -1,10 +1,12 @@ -# 宏那些事 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/macro/) 同步生成,网站版本含多语言与更新排版。 + +# C++ 宏与条件编译 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 1.宏中包含特殊符号 @@ -113,7 +115,7 @@ int main() { } ``` -上述代码见:[sig_examp.cpp](sig_examp.cpp) +上述代码见:[sig_examp.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/macro/sig_examp.cpp) ## 2.do{...}while(0)的使用 @@ -226,7 +228,6 @@ int fc() } ``` -上述代码见:[do_while.cpp](do_while.cpp) - -学习文章: +上述代码见:[do_while.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/macro/do_while.cpp) +学习文章: \ No newline at end of file diff --git a/basic_content/maohao/README.md b/basic_content/maohao/README.md index d78b3e5..186ab07 100644 --- a/basic_content/maohao/README.md +++ b/basic_content/maohao/README.md @@ -1,14 +1,15 @@ -# :: 范围解析运算符那些事 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/namespace/) 同步生成,网站版本含多语言与更新排版。 + +# C++ 命名空间 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) - 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间 - 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的 - 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的 -具体代码见:[maohao.cpp](maohao.cpp) - +具体代码见:[maohao.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/maohao/maohao.cpp) \ No newline at end of file diff --git a/basic_content/pointer_refer/README.md b/basic_content/pointer_refer/README.md index ba4b0dd..a364ed0 100644 --- a/basic_content/pointer_refer/README.md +++ b/basic_content/pointer_refer/README.md @@ -1,10 +1,12 @@ -# 引用与指针那些事 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/pointer-vs-reference/) 同步生成,网站版本含多语言与更新排版。 + +# C++ 指针与引用 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 1.引用与指针 @@ -171,4 +173,4 @@ End of assembler dump. C++中引入了引用操作,在对引用的使用加了更多限制条件的情况下,保证了引用使用的安全性和便捷性,还可以保持代码的优雅性。在适合的情况使用适合的操作,引用的使用可以一定程度避免“指针满天飞”的情况,对于提升程序稳定性也有一定的积极意义。最后,指针与引用底层实现都是一样的,不用担心两者的性能差距。 -上述部分参考自: +上述部分参考自: \ No newline at end of file diff --git a/basic_content/sizeof/README.md b/basic_content/sizeof/README.md index 6ddb25c..4f665ca 100644 --- a/basic_content/sizeof/README.md +++ b/basic_content/sizeof/README.md @@ -1,4 +1,6 @@ -# 类大小计算 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/sizeof/) 同步生成,网站版本含多语言与更新排版。 + +# C++ sizeof 运算符 首先来个总结,然后下面给出实际例子,实战! @@ -194,11 +196,10 @@ class C : virtual public A, virtual public B int main() { /** - * @brief 8 8 16 派生类虚继承多个虚函数,会继承所有虚函数的vptr + * @brief GCC: 8 8 16(虚基类指针与虚函数表指针合并);MSVC/Windows: 8 8 32(虚基类指针与虚函数表指针分别存储),结果与编译器相关,见 issue #246 */ cout< 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/static/) 同步生成,网站版本含多语言与更新排版。 + +# C++ static 静态成员与生存期 ## 关于作者 微信公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) 当与不同类型一起使用时,Static关键字具有不同的含义。我们可以使用static关键字: @@ -277,3 +279,21 @@ int main() return 0; } ``` +### 6.static 的内部链接(限定访问范围) + +感谢 [issue #142](https://github.com/Light-City/CPlusPlusThings/issues/142) 指出:static 修饰全局变量/函数时,还起到**限定访问范围**的作用——将其链接性从外部链接(external linkage)改为内部链接(internal linkage),效果类似匿名命名空间: + +- 非 static 的全局变量:其他编译单元可以通过 `extern` 声明访问; +- static 全局变量/函数:仅当前编译单元可见,`extern` 声明也无法链接到(上面的 undefined reference 正是这个原因)。 + +现代 C++ 中更推荐用**匿名命名空间**表达"仅本文件可见"(对类型同样有效),但理解 static 的这一层含义在阅读旧代码时非常重要。 + +```cpp +// file1.cpp +static int hidden = 42; // 内部链接:其他文件 extern 不可见 +int visible = 1; // 外部链接:其他文件可 extern 访问 + +namespace { // 匿名命名空间:等价且更现代的写法 + int also_hidden = 7; +} +``` diff --git a/basic_content/struct/README.md b/basic_content/struct/README.md index 147723c..4eea260 100644 --- a/basic_content/struct/README.md +++ b/basic_content/struct/README.md @@ -1,10 +1,12 @@ -# 一文搞懂C和C++中struct +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/struct/) 同步生成,网站版本含多语言与更新排版。 + +# C++ struct 结构体 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 1.C中struct @@ -52,7 +54,7 @@ int main() { I am Base func ``` -完整代码见:[struct_func.c](./struct_func.c) +完整代码见:[struct_func.c](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/struct/struct_func.c) ## 2.C++中struct @@ -135,7 +137,7 @@ int main() { } ``` -完整代码见:[struct_func.cpp](struct_func.cpp) +完整代码见:[struct_func.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/struct/struct_func.cpp) > 继承案例 @@ -167,7 +169,7 @@ int main() { } ``` -完整代码见:[ext_struct_func.cpp](./ext_struct_func.cpp) +完整代码见:[ext_struct_func.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/struct/ext_struct_func.cpp) > 同名函数 @@ -212,7 +214,7 @@ int main() { return 0; } ``` -完整代码见:[struct_func_func.cpp](./struct_func_func.cpp) +完整代码见:[struct_func_func.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/struct/struct_func_func.cpp) ## 3.总结 @@ -224,4 +226,4 @@ int main() { | 在C结构体声明中不能使用C++访问修饰符。 | public、protected、private 在C++中可以使用。 | | 在C中定义结构体变量,如果使用了下面定义必须加struct。 | 可以不加struct | | 结构体不能继承(没有这一概念)。 | 可以继承 | -| 若结构体的名字与函数名相同,可以正常运行且正常的调用! | 若结构体的名字与函数名相同,使用结构体,只能使用带struct定义! | +| 若结构体的名字与函数名相同,可以正常运行且正常的调用! | 若结构体的名字与函数名相同,使用结构体,只能使用带struct定义! | \ No newline at end of file diff --git a/basic_content/struct_class/README.md b/basic_content/struct_class/README.md index 119e039..72a45eb 100644 --- a/basic_content/struct_class/README.md +++ b/basic_content/struct_class/README.md @@ -1,12 +1,14 @@ -# struct与class区别 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/struct-vs-class/) 同步生成,网站版本含多语言与更新排版。 + +# C++ struct 与 class 区别 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) -关于C与C++中struct内容:见[struct那些事](../struct) +关于C与C++中struct内容:见[struct那些事](struct.md) 总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。 @@ -16,4 +18,4 @@ 默认的继承访问权限。struct 是 public 的,class 是 private 的。 -struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。 +struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。 \ No newline at end of file diff --git a/basic_content/this/README.md b/basic_content/this/README.md index f950237..92182f0 100644 --- a/basic_content/this/README.md +++ b/basic_content/this/README.md @@ -1,10 +1,12 @@ -# this指针那些事 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/this/) 同步生成,网站版本含多语言与更新排版。 + +# C++ this 指针 ## 关于作者 微信公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 1.this指针 @@ -74,16 +76,16 @@ int main(){ 为了验证this指针是哪一个,现在在`add_age`处添加断点,运行后如下: -![thiscontrust](./img/thiscontrust.png) +![thiscontrust](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/basic_content/this/img/thiscontrust.png) -![genthis](./img/genthis.png) +![genthis](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/basic_content/this/img/genthis.png) 会发现编译器自动为我们加上`A* const`,而不是`A const *this`! 紧接着,上述还有个常函数,那么我们在对`get_age`添加断点,如下: -![constthis](./img/constthis.png) +![constthis](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/basic_content/this/img/constthis.png) 会发现编译器把上述的this,变为`const A* const`,这个大家也能想到,因为这个函数是const函数,那么针对const函数,它只能访问const变量与const函数,不能修改其他变量的值,所以需要一个this指向不能修改的变量,那就是`const A*`,又由于本身this是`const`指针,所以就为`const A* const`! -总结:this在成员函数的开始执行前构造,在成员的执行结束后清除。上述的get_age函数会被解析成`get_age(const A * const this)`,`add_age`函数会被解析成`add_age(A* const this,int a)`。在C++中类和结构是只有一个区别的:类的成员默认是private,而结构是public。this是类的指针,如果换成结构,那this就是结构的指针了。 +总结:this在成员函数的开始执行前构造,在成员的执行结束后清除。上述的get_age函数会被解析成`get_age(const A * const this)`,`add_age`函数会被解析成`add_age(A* const this,int a)`。在C++中类和结构是只有一个区别的:类的成员默认是private,而结构是public。this是类的指针,如果换成结构,那this就是结构的指针了。 \ No newline at end of file diff --git a/basic_content/union/README.md b/basic_content/union/README.md index 5421d42..c9e2b7c 100644 --- a/basic_content/union/README.md +++ b/basic_content/union/README.md @@ -1,10 +1,12 @@ -# UNION那些事 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/union/) 同步生成,网站版本含多语言与更新排版。 + +# C++ union 联合体 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) 联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点: @@ -15,4 +17,4 @@ - 不能含有虚函数 - 匿名 union 在定义所在作用域可直接访问 union 成员 - 匿名 union 不能包含 protected 成员或 private 成员 -- 全局匿名联合必须是静态(static)的 +- 全局匿名联合必须是静态(static)的 \ No newline at end of file diff --git a/basic_content/using/README.md b/basic_content/using/README.md index 1e50179..14c9f53 100644 --- a/basic_content/using/README.md +++ b/basic_content/using/README.md @@ -1,9 +1,12 @@ -# using那些事 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/using/) 同步生成,网站版本含多语言与更新排版。 + +# C++ using 声明与别名 + ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 基本使用 @@ -48,7 +51,7 @@ int main() return 0; } ``` -完整代码见:[using_global.cpp](using_global.cpp) +完整代码见:[using_global.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/using/using_global.cpp) ## 改变访问性 ``` @@ -67,7 +70,7 @@ protected: ``` 类Derived私有继承了Base,对于它来说成员变量n和成员函数size都是私有的,如果使用了using语句,可以改变他们的可访问性,如上述例子中,size可以按public的权限访问,n可以按protected的权限访问。 -完整代码见:[derived_base.cpp](derived_base.cpp) +完整代码见:[derived_base.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/using/derived_base.cpp) ## 函数重载 在继承过程中,派生类可以覆盖重载函数的0个或多个实例,一旦定义了一个重载版本,那么其他的重载版本都会变为不可见。 @@ -106,7 +109,7 @@ int main() ``` 如上代码中,在派生类中使用using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类的作用域中。此时,派生类只需要定义其特有的函数就行了,而无需为继承而来的其他函数重新定义。 -完整代码见:[using_derived.cpp](using_derived.cpp) +完整代码见:[using_derived.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/using/using_derived.cpp) ## 取代typedef C中常用typedef A B这样的语法,将B定义为A类型,也就是给A类型一个别名B @@ -117,4 +120,4 @@ C中常用typedef A B这样的语法,将B定义为A类型,也就是给A类 typedef vector V1; using V2 = vector; ``` -完整代码见:[using_typedef.cpp](using_typedef.cpp) +完整代码见:[using_typedef.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/using/using_typedef.cpp) \ No newline at end of file diff --git a/basic_content/virtual/README.md b/basic_content/virtual/README.md index b3f6b13..22a661e 100644 --- a/basic_content/virtual/README.md +++ b/basic_content/virtual/README.md @@ -1,24 +1,26 @@ -# virtual那些事 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/virtual/) 同步生成,网站版本含多语言与更新排版。 + +# C++ virtual 虚函数 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 1.虚函数与运行多态 -对应的代码:[emp.cpp](./set1/emp.cpp) +对应的代码:[emp.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set1/emp.cpp) **虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。** ## 2.vptr与vtable -见[vptr_vtable那些事](../vptr_vtable) +见[vptr_vtable那些事](vptr-vtable.md) ## 3.虚函数中默认参数 -对应的代码:[default_arg.cpp](./set2/default_arg.cpp) +对应的代码:[default_arg.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set2/default_arg.cpp) **默认参数是静态绑定的,虚函数是动态绑定的。 默认参数的使用需要看指针或者引用本身的类型,而不是对象的类型**。 @@ -34,7 +36,7 @@ static成员函数不属于任何类对象或类实例,所以即使给此函 虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,静态成员函数没有this指针,所以无法访问vptr。 -代码学习:[static_error.cpp ](./set3/static_error.cpp ) +代码学习:[static_error.cpp ](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set3/static_error.cpp) (2)**构造函数可以为虚函数吗?** @@ -48,9 +50,9 @@ static成员函数不属于任何类对象或类实例,所以即使给此函 代码学习: -- [copy_consrtuct.cpp](./set3/copy_consrtuct.cpp) +- [copy_consrtuct.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set3/copy_consrtuct.cpp) -- [vir_con.cpp](./set3/vir_con.cpp) +- [vir_con.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set3/vir_con.cpp) (3)**析构函数可以为虚函数吗?** @@ -58,9 +60,9 @@ static成员函数不属于任何类对象或类实例,所以即使给此函 代码学习: -- [full_virde.cpp](./set3/full_virde.cpp) +- [full_virde.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set3/full_virde.cpp) -- [vir_de.cpp ](./set3/vir_de.cpp) +- [vir_de.cpp ](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set3/vir_de.cpp) (4)**虚函数可以为私有函数吗?** @@ -69,8 +71,8 @@ static成员函数不属于任何类对象或类实例,所以即使给此函 代码学习: -- [virtual_function.cpp](./set3/virtual_function.cpp) -- [virtual_function1.cpp](./set3/virtual_function1.cpp) +- [virtual_function.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set3/virtual_function.cpp) +- [virtual_function1.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set3/virtual_function1.cpp) (5)**虚函数可以被内联吗?** @@ -82,9 +84,9 @@ static成员函数不属于任何类对象或类实例,所以即使给此函 代码学习: -- [virtual_inline.cpp](./set3/virtual_inline.cpp) +- [virtual_inline.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set3/virtual_inline.cpp) -- [inline_virtual.cpp](./set3/inline_virtual.cpp) +- [inline_virtual.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set3/inline_virtual.cpp) ## 5.RTTI与dynamic_cast @@ -94,9 +96,9 @@ RTTI(Run-Time Type Identification),通过运行时类型信息程序能够 代码学习: -- [rtti.cpp](./set4/rtti.cpp) -- [warn_rtti.cpp](./set4/warn_rtti.cpp) +- [rtti.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set4/rtti.cpp) +- [warn_rtti.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/virtual/set4/warn_rtti.cpp) ## 6.纯虚函数和抽象类 -见[纯虚函数和抽象类那些事](../abstract) +见[纯虚函数和抽象类那些事](abstract.md) \ No newline at end of file diff --git a/basic_content/volatile/README.md b/basic_content/volatile/README.md index 25221cd..f0cd753 100644 --- a/basic_content/volatile/README.md +++ b/basic_content/volatile/README.md @@ -1,8 +1,12 @@ +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/volatile/) 同步生成,网站版本含多语言与更新排版。 + +# C++ volatile 关键字 + ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) ## 1.volatile @@ -117,6 +121,5 @@ return a * a; 代码学习: -- [noopt_vola.cpp](./noopt_vola.cpp) -- [volatile.cpp](./volatile.cpp) - +- [noopt_vola.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/volatile/noopt_vola.cpp) +- [volatile.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/basic_content/volatile/volatile.cpp) \ No newline at end of file diff --git a/basic_content/vptr_vtable/README.md b/basic_content/vptr_vtable/README.md index eaadf24..d9acc47 100644 --- a/basic_content/vptr_vtable/README.md +++ b/basic_content/vptr_vtable/README.md @@ -1,10 +1,12 @@ -# 深入浅出C++虚函数的vptr与vtable +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/vptr-vtable/) 同步生成,网站版本含多语言与更新排版。 + +# C++ 虚指针与虚表机制 ## 关于作者: 个人公众号: -![](../img/wechat.jpg) +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) @@ -24,7 +26,7 @@ 开始看代码之前,为了方便大家理解,这里给出调用图: -![base](./img/base.jpg) +![base](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/basic_content/vptr_vtable/img/base.jpg) 代码全部遵循标准的注释风格,相信大家看了就会明白,不明白的话,可以留言! @@ -215,5 +217,4 @@ Dump of assembler code for function Derived::fun2(): 0x0000000000400efd <+41>: leaveq 0x0000000000400efe <+42>: retq End of assembler dump. -``` - +``` \ No newline at end of file diff --git a/codingStyleIdioms/RAII/README.md b/codingStyleIdioms/RAII/README.md new file mode 100644 index 0000000..b59f209 --- /dev/null +++ b/codingStyleIdioms/RAII/README.md @@ -0,0 +1,314 @@ +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/raii/) 同步生成,网站版本含多语言与更新排版。 + +# C++ RAII 资源管理 + +## 0.导语 + +在C语言中,有三种类型的内存分配:静态、自动和动态。静态变量是嵌入在源文件中的常数,因为它们有已知的大小并且从不改变,所以它们并不那么有趣。自动分配可以被认为是堆栈分配——当一个词法块进入时分配空间,当该块退出时释放空间。它最重要的特征与此直接相关。在C99之前,自动分配的变量需要在编译时知道它们的大小。这意味着任何字符串、列表、映射以及从这些派生的任何结构都必须存在于堆中的动态内存中。 + +程序员使用四个基本操作明确地分配和释放动态内存:malloc、realloc、calloc和free。前两个不执行任何初始化,内存可能包含碎片。除了自由,他们都可能失败。在这种情况下,它们返回一个空指针,其访问是未定义的行为;在最好的情况下,你的程序会崩溃。在最坏的情况下,你的程序看起来会工作一段时间,在崩溃前处理垃圾数据。 + +例如: + +```cpp +int main() { + char *str = (char *) malloc(7); + strcpy(str, "toptal"); + printf("char array = \"%s\" @ %u\n", str, str); + + str = (char *) realloc(str, 11); + strcat(str, ".com"); + printf("char array = \"%s\" @ %u\n", str, str); + + free(str); + + return(0); +} +``` + +输出: + +```cpp +char array = "toptal" @ 2762894960 +char array = "toptal.com" @ 2762894960 +``` + +尽管代码很简单,但它已经包含了一个反模式和一个有问题的决定。在现实生活中,你不应该直接写字节数,而应该使用sizeof函数。类似地,我们将char *数组精确地分配给我们需要的字符串大小的两倍(比字符串长度多一倍,以说明空终止),这是一个相当昂贵的操作。一个更复杂的程序可能会构建一个更大的字符串缓冲区,允许字符串大小增长。 + +## 1.RAII的发明:新希望 + +至少可以说,所有手动管理都是令人不快的。 在80年代中期,Bjarne Stroustrup为他的全新语言C ++发明了一种新的范例。 他将其称为“资源获取就是初始化”,其基本见解如下:**可以指定对象具有构造函数和析构函数,这些构造函数和析构函数在适当的时候由编译器自动调用,这为管理给定对象的内存提供了更为方便的方法。** 需要,并且该技术对于不是内存的资源也很有用。 + +意味着上面的例子在c++中更简洁: + +```cpp +int main() { + std::string str = std::string ("toptal"); + std::cout << "string object: " << str << " @ " << &str << "\n"; + + str += ".com"; + std::cout << "string object: " << str << " @ " << &str << "\n"; + + return(0); +} +``` + +输出: + +```cpp +string object: toptal @ 0x7fffa67b9400 +string object: toptal.com @ 0x7fffa67b9400 +``` + +在上述例子中,我们没有手动内存管理!构造string对象,调用重载方法,并在函数退出时自动销毁。不幸的是,同样的简单也会导致其他问题。让我们详细地看一个例子: + +```cpp +vector read_lines_from_file(string &file_name) { + vector lines; + string line; + + ifstream file_handle (file_name.c_str()); + while (file_handle.good() && !file_handle.eof() && file_handle.peek()!=EOF) { + getline(file_handle, line); + lines.push_back(line); + } + + file_handle.close(); + + return lines; +} + +int main(int argc, char* argv[]) { + // get file name from the first argument + string file_name (argv[1]); + int count = read_lines_from_file(file_name).size(); + cout << "File " << file_name << " contains " << count << " lines."; + + return 0; +} +``` + +输出: + +```cpp +File makefile contains 37 lines. +``` + +这看起来很简单。`vector`被填满、返回和调用。然而,作为关心性能的高效程序员,这方面的一些问题困扰着我们:在return语句中,由于使用了值语义,`vector`在销毁之前不久就被复制到一个新`vector`中。 + +> 在现代C ++中,这不再是严格的要求了。 C ++ 11引入了移动语义的概念,其中将原点保留在有效状态(以便仍然可以正确销毁)但未指定状态。 对于编译器而言,返回调用是最容易优化以优化语义移动的情况,因为它知道在进行任何进一步访问之前不久将销毁源。 但是,该示例的目的是说明为什么人们在80年代末和90年代初发明了一大堆垃圾收集的语言,而在那个时候C ++ move语义不可用。 + +对于数据量比较大的文件,这可能会变得昂贵。 让我们对其进行优化,只返回一个指针。 语法进行了一些更改,但其他代码相同: + +```cpp +vector * read_lines_from_file(string &file_name) { + vector * lines; + string line; + + ifstream file_handle (file_name.c_str()); + while (file_handle.good() && !file_handle.eof() && file_handle.peek()!=EOF) { + getline(file_handle, line); + lines->push_back(line); + } + + file_handle.close(); + + return lines; +} +int main(int argc, char* argv[]) { + // get file name from the first argument + string file_name (argv[1]); + int count = read_lines_from_file(file_name).size(); + cout << "File " << file_name << " contains " << count << " lines."; + + return 0; +} +``` + +输出: + +``` +Segmentation fault (core dumped) +``` + +程序崩溃!我们只需要将上述的`lines`进行内存分配: + +```cpp +vector * lines = new vector; +``` + +这样就可以运行了! + +不幸的是,尽管这看起来很完美,但它仍然有一个缺陷:它会泄露内存。在C++中,指向堆的指针在不再需要后必须手动删除;否则,一旦最后一个指针超出范围,该内存将变得不可用,并且直到进程结束时操作系统对其进行管理后才会恢复。惯用的现代C++将在这里使用`unique_ptr`,它实现了期望的行为。它删除指针超出范围时指向的对象。然而,这种行为直到C++11才成为语言的一部分。 + +在这里,可以直接使用C++11之前的语法,只是把main中改一下即可: + +```cpp +vector * read_lines_from_file(string &file_name) { + vector * lines = new vector; + string line; + + ifstream file_handle (file_name.c_str()); + while (file_handle.good() && !file_handle.eof() && file_handle.peek()!=EOF) { + getline(file_handle, line); + lines->push_back(line); + } + + file_handle.close(); + + return lines; +} + +int main(int argc, char* argv[]) { + // get file name from the first argument + string file_name (argv[1]); + vector * file_lines = read_lines_from_file(file_name); + int count = file_lines->size(); + delete file_lines; + cout << "File " << file_name << " contains " << count << " lines."; + + return 0; +} +``` + +手动去分配内存与释放内存。 + +**不幸的是,随着程序扩展到上述范围之外,很快就变得更加难以推理指针应该在何时何地被删除。当一个函数返回指针时,你现在拥有它吗?您应该在完成后自己删除它,还是它属于某个稍后将被一次性释放的数据结构?一方面出错,内存泄漏,另一方面出错,你已经破坏了正在讨论的数据结构和其他可能的数据结构,因为它们试图取消引用现在不再有效的指针。** + +## 2.“使用垃圾收集器,flyboy!” + +垃圾收集器不是一项新技术。 它们由John McCarthy在1959年为Lisp发明。 1980年,随着Smalltalk-80的出现,垃圾收集开始成为主流。 但是,1990年代代表了该技术的真正发芽:在1990年至2000年之间,发布了多种语言,所有语言都使用一种或另一种垃圾回收:Haskell,Python,Lua,Java,JavaScript,Ruby,OCaml 和C#是最著名的。 + +什么是垃圾收集? 简而言之,这是一组用于自动执行手动内存管理的技术。 它通常作为具有手动内存管理的语言(例如C和C ++)的库提供,但在需要它的语言中更常用。 最大的优点是程序员根本不需要考虑内存。 都被抽象了。 例如,相当于我们上面的文件读取代码的Python就是这样: + +```python +def read_lines_from_file(file_name): + lines = [] + with open(file_name) as fp: + for line in fp: + lines.append(line) + return lines + +if __name__ == '__main__': + import sys + file_name = sys.argv[1] + count = len(read_lines_from_file(file_name)) + print("File {} contains {} lines.".format(file_name, count)) +``` + +行数组是在第一次分配给它时出现的,并且不复制到调用范围就返回。 由于时间不确定,它会在超出该范围后的某个时间被垃圾收集器清理。 有趣的是,在Python中,用于非内存资源的RAII不是惯用语言。 允许-我们可以简单地编写`fp = open(file_name)`而不是使用with块,然后让GC清理。 但是建议的模式是在可能的情况下使用上下文管理器,以便可以在确定的时间释放它们。 + +尽管简化了内存管理,但要付出很大的代价。 在引用计数垃圾回收中,所有变量赋值和作用域出口都会获得少量成本来更新引用。 在标记清除系统中,在GC清除内存的同时,所有程序的执行都以不可预测的时间间隔暂停。 这通常称为世界停止事件。 同时使用这两种系统的Python之类的实现都会受到两种惩罚。 这些问题降低了垃圾收集语言在性能至关重要或需要实时应用程序的情况下的适用性。 即使在以下玩具程序上,也可以看到实际的性能下降: + +```cpp +$ make cpp && time ./c++ makefile +g++ -o c++ c++.cpp +File makefile contains 38 lines. +real 0m0.016s +user 0m0.000s +sys 0m0.015s + +$ time python3 python3.py makefile +File makefile contains 38 lines. + +real 0m0.041s +user 0m0.015s +sys 0m0.015s +``` + +Python版本的实时时间几乎是C ++版本的三倍。 尽管并非所有这些差异都可以归因于垃圾收集,但它仍然是可观的。 + +## 3.所有权:RAII觉醒 + +我们知道对象的生存期由其范围决定。 但是,有时我们需要创建一个对象,该对象与创建对象的作用域无关,这是有用的,或者很有用。 在C ++中,运算符new用于创建这样的对象。 为了销毁对象,可以使用运算符delete。 由new操作员创建的对象是动态分配的,即在动态内存(也称为堆或空闲存储)中分配。 因此,由new创建的对象将继续存在,直到使用delete将其明确销毁为止。 + +使用new和delete时可能发生的一些错误是: + +- 对象(或内存)泄漏:使用new分配对象,而忘记删除该对象。 + +- 过早删除(或悬挂引用):持有指向对象的另一个指针,删除该对象,然而还有其他指针在引用它。 + +- 双重删除:尝试两次删除一个对象。 + +通常,范围变量是首选。 但是,RAII可以用作new和delete的替代方法,以使对象独立于其范围而存在。 这种技术包括将指针分配到在堆上分配的对象,并将其放在句柄/管理器对象中。 后者具有一个析构函数,将负责销毁该对象。 这将确保该对象可用于任何想要访问它的函数,并且该对象在句柄对象的生存期结束时将被销毁,而无需进行显式清理。 + +来自C ++标准库的使用RAII的示例为std :: string和std :: vector。 + +考虑这段代码: + +```cpp +void fn(const std::string& str) +{ + std::vector vec; + for (auto c : str) + vec.push_back(c); + // do something +} +``` + +当创建`vector`,并将元素推入`vector`时,您不必担心分配和取消分配此类元素内存。 `vector`使用new为其堆上的元素分配空间,并使用delete释放该空间。 作为vector的用户,您无需关心实现细节,并且会相信vector不会泄漏。 在这种情况下,向量是其元素的句柄对象。 + +标准库中使用RAII的其他示例是std :: shared_ptr,std :: unique_ptr和std :: lock_guard。 + +该技术的另一个名称是SBRM,是范围绑定资源管理的缩写。 + +现在,我们将上述读取文件例子,进行修改: + +```cpp +#include +#include +#include +#include +#include + +using namespace std; +unique_ptr> read_lines_from_file(string &file_name) { + unique_ptr> lines(new vector); + string line; + + ifstream file_handle (file_name.c_str()); + while (file_handle.good() && !file_handle.eof() && file_handle.peek()!=EOF) { + getline(file_handle, line); + lines->push_back(line); + } + + file_handle.close(); + + return lines; +} +int main(int argc, char* argv[]) { + // get file name from the first argument + string file_name (argv[1]); + int count = read_lines_from_file(file_name).get()->size(); + cout << "File " << file_name << " contains " << count << " lines."; + + return 0; +} +``` + +## 4.只有在最后,你才意识到RAII的真正力量。 + +自从编译器发明以来,手动内存管理是程序员一直在想办法避免的噩梦。 RAII是一种很有前途的模式,但由于没有一些奇怪的解决方法,它根本无法用于堆分配的对象,因此在C ++中会受到影响。 因此,在90年代出现了垃圾收集语言的爆炸式增长,旨在使程序员生活更加愉快,即使以性能为代价。 + +最后,RAII总结如下: + +- 资源在析构函数中被释放 + +- 该类的实例是堆栈分配的 +- 资源是在构造函数中获取的。 + +RAII代表“资源获取是初始化”。 + +常见的例子有: + +- 文件操作 + +- 智能指针 + +- 互斥量 + +## 5.参考文章 + +> [1. https://www.toptal.com/software/eliminating-garbage-collector#remote-developer-job](https://www.toptal.com/software/eliminating-garbage-collector#remote-developer-job) + +> [2. https://stackoverflow.com/questions/2321511/what-is-meant-by-resource-acquisition-is-initialization-raii](https://stackoverflow.com/questions/2321511/what-is-meant-by-resource-acquisition-is-initialization-raii) \ No newline at end of file diff --git a/codingStyleIdioms/classInitializers/README.md b/codingStyleIdioms/classInitializers/README.md new file mode 100644 index 0000000..de8e454 --- /dev/null +++ b/codingStyleIdioms/classInitializers/README.md @@ -0,0 +1,212 @@ +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/class-initializer-list/) 同步生成,网站版本含多语言与更新排版。 + +# C++ 类初始化列表惯用法 + +- const成员的初始化只能在构造函数初始化列表中进行 +- 引用成员的初始化也只能在构造函数初始化列表中进行 +- 对象成员(对象成员所对应的类没有默认构造函数)的初始化,也只能在构造函数初始化列表中进行 + +## 类之间嵌套 + +**第一种: 使用初始化列表。** + + +```cpp +class Animal { +public: + Animal() { + std::cout << "Animal() is called" << std::endl; + } + + Animal(const Animal &) { + std::cout << "Animal (const Animal &) is called" << std::endl; + } + + Animal &operator=(const Animal &) { + std::cout << "Animal & operator=(const Animal &) is called" << std::endl; + return *this; + } + + ~Animal() { + std::cout << "~Animal() is called" << std::endl; + } +}; + +class Dog { +public: + Dog(const Animal &animal) : __animal(animal) { + std::cout << "Dog(const Animal &animal) is called" << std::endl; + } + + ~Dog() { + std::cout << "~Dog() is called" << std::endl; + } + +private: + Animal __animal; +}; + +int main() { + Animal animal; + std::cout << std::endl; + Dog d(animal); + std::cout << std::endl; + return 0; +} +``` + +运行结果: +```cpp +Animal() is called + +Animal (const Animal &) is called +Dog(const Animal &animal) is called + +~Dog() is called +~Animal() is called +~Animal() is called +``` + +依次分析从上到下: + +main函数中`Animal animal;`调用默认构造。 + +`Dog d(animal);`等价于: + +``` +Animal __animal = animal; +``` +实际上就是调用了拷贝构造,因此输出了: +``` +Animal (const Animal &) is called +``` +再然后打印Dog的构造函数里面的输出。 + +最后调用析构,程序结束。 + +**第二种:构造函数赋值来初始化对象。** + +构造函数修改如下: +```cpp +Dog(const Animal &animal) { + __animal = animal; + std::cout << "Dog(const Animal &animal) is called" << std::endl; +} +``` +此时输出结果: +``` +Animal() is called + +Animal() is called +Animal & operator=(const Animal &) is called +Dog(const Animal &animal) is called + +~Dog() is called +~Animal() is called +~Animal() is called +``` + +于是得出: + +当调用`Dog d(animal);`时,等价于: + +先定义对象,再进行赋值,因此先调用了默认构造,再调用=操作符重载函数。 + +```cpp +// 假设之前已经有了animal对象 +Animal __animal; +__animal = animal; +``` + +> 小结 + +通过上述我们得出如下结论: + +- **类中包含其他自定义的class或者struct,采用初始化列表,实际上就是创建对象同时并初始化** +- **而采用类中赋值方式,等价于先定义对象,再进行赋值,一般会先调用默认构造,在调用=操作符重载函数。** + +## 无默认构造函数的继承关系中 + +现考虑把上述的关系改为继承,并修改Animal与Dog的构造函数,如下代码: + +```cpp +class Animal { +public: + Animal(int age) { + std::cout << "Animal(int age) is called" << std::endl; + } + + Animal(const Animal & animal) { + std::cout << "Animal (const Animal &) is called" << std::endl; + } + + Animal &operator=(const Animal & amimal) { + std::cout << "Animal & operator=(const Animal &) is called" << std::endl; + return *this; + } + + ~Animal() { + std::cout << "~Animal() is called" << std::endl; + } +}; + +class Dog : Animal { +public: + Dog(int age) : Animal(age) { + std::cout << "Dog(int age) is called" << std::endl; + } + + ~Dog() { + std::cout << "~Dog() is called" << std::endl; + } + +}; +``` + +上述是通过初始化列表给基类带参构造传递参数,如果不通过初始化列表传递,会发生什么影响? + +去掉初始化列表 +``` +Dog(int age) { + std::cout << "Dog(int age) is called" << std::endl; +} +``` +运行程序: + +``` +error: no matching function for call to ‘Animal::Animal()’ +``` + +由于在Animal中没有默认构造函数,所以报错,遇到这种问题属于灾难性的,我们应该尽量避免,可以通过初始化列表给基类的构造初始化。 + + +## 类中const数据成员、引用数据成员 + +特别是引用数据成员,必须用初始化列表初始化,而不能通过赋值初始化! + +例如:在上述的Animal中添加私有成员,并修改构造函数: +```cpp +class Animal { +public: + Animal(int age,std::string name) { + std::cout << "Animal(int age) is called" << std::endl; + } +private: + int &age_; + const std::string name_; +}; +``` + +报下面错误: + +```cpp +error: uninitialized reference member in ‘int&’ +``` + +应该改为下面: + +```cpp +Animal(int age, std::string name) : age_(age), name_(name) { + std::cout << "Animal(int age) is called" << std::endl; +} +``` \ No newline at end of file diff --git a/codingStyleIdioms/copy-swap/README.md b/codingStyleIdioms/copy-swap/README.md new file mode 100644 index 0000000..d2e4bf0 --- /dev/null +++ b/codingStyleIdioms/copy-swap/README.md @@ -0,0 +1,207 @@ +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/copy-swap/) 同步生成,网站版本含多语言与更新排版。 + +# C++ copy-and-swap 惯用法 + +> 为什么我们需要复制和交换习惯? + +任何管理资源的类(包装程序,如智能指针)都需要实现big three。尽管拷贝构造函数和析构函数的目标和实现很简单。 + +但是复制分配运算符无疑是最细微和最困难的。 + +> 应该怎么做?需要避免什么陷阱? + +copy-swap是解决方案,可以很好地协助赋值运算符实现两件事:避免代码重复,并提供强大的**异常保证**。 + +>它是如何工作的? + +**从概念上讲,它通过使用拷贝构造函数的功能来创建数据的本地副本,然后使用交换功能获取复制的数据,将旧数据与新数据交换来工作。然后,临时副本将销毁,并随身携带旧数据。我们剩下的是新数据的副本。** + +为了使用copy-swap,我们需要三件事: + +- 一个有效的拷贝构造函数 +- 一个有效的析构函数(两者都是任何包装程序的基础,因此无论如何都应完整)以及交换功能。 + +交换函数是一种不抛异常函数,它交换一个类的两个对象或者成员。我们可能很想使用std :: swap而不是提供我们自己的方法,但这是不可能的。 std :: swap在实现中使用了copy-constructor和copy-assignment运算符,我们最终将尝试根据自身定义赋值运算符! + +(不仅如此,对swap的无条件调用将使用我们的自定义swap运算符,从而跳过了std :: swap会导致的不必要的类构造和破坏。) + +具体例子如下: + +```cpp +namespace A { + template + class smart_ptr { + public: + smart_ptr() noexcept : ptr_(new T()) { + + } + + smart_ptr(const T &ptr) noexcept : ptr_(new T(ptr)) { + + } + + smart_ptr(smart_ptr &rhs) noexcept { + ptr_ = rhs.release(); // 释放所有权,此时rhs的ptr_指针为nullptr + } + + void swap(smart_ptr &rhs) noexcept { // noexcept == throw() 保证不抛出异常 + using std::swap; + swap(ptr_, rhs.ptr_); + } + + T *release() noexcept { + T *ptr = ptr_; + ptr_ = nullptr; + return ptr; + } + + T *get() const noexcept { + return ptr_; + } + + private: + T *ptr_; + }; + +// 提供一个非成员swap函数for ADL(Argument Dependent Lookup) + template + void swap(A::smart_ptr &lhs, A::smart_ptr &rhs) noexcept { + lhs.swap(rhs); + } +} +// 注释开启,会引发ADL冲突 +//namespace std { +// // 提供一个非成员swap函数for ADL(Argument Dependent Lookup) +// template +// void swap(A::smart_ptr &lhs, A::smart_ptr &rhs) noexcept { +// lhs.swap(rhs); +// } +// +//} + +int main() { + + using std::swap; + A::smart_ptr s1("hello"), s2("world"); + // 交换前 + std::cout << *s1.get() << " " << *s2.get() << std::endl; + swap(s1, s2); // 这里swap 能够通过Koenig搜索或者说ADL根据s1与s2的命名空间来查找swap函数 + // 交换后 + std::cout << *s1.get() << " " << *s2.get() << std::endl; + s1=s2; +} +``` + +现在为了让上述的`s1=s2`完成工作,必须实现赋值运算符。 + +> 方法1 + +为了避免自赋值,通常采用下面写法 。 + +不好! + +不具备异常安全,只具备自我赋值安全性 + +```cpp +smart_ptr &operator=(const smart_ptr &rhs) { + if (*this != rhs) { + delete ptr_; + ptr_ = new T(rhs.ptr_); // 当new 发生异常,此时ptr_指向的而是一块被删除区域,而不是被赋值对象的区域 + return *this; + } + return *this; +} +``` + +> 方法2 + +如果new出现异常,ptr_会保持原装! 也可以处理自我赋值! 还是不够好! + +**这样就会导致代码膨胀,于是导致了另一个问题:代码冗余** + +```cpp +// 方法2:如果new出现异常,ptr_会保持原装! 也可以处理自我赋值! 还是不够好! +smart_ptr &operator=(const smart_ptr &rhs) { + T *origin = ptr_; + ptr_ = new T(rhs.ptr_); + delete origin; + return *this; +} +``` + +> 方法3 + +copy and swap 很好! + +```cpp +smart_ptr &operator=(smart_ptr &rhs) noexcept { + smart_ptr tmp(rhs); + swap(tmp); + return *this; +} +``` + +> 方法4 + +改为传值,同方法3! + +```cpp +smart_ptr &operator=(smart_ptr rhs) noexcept { + swap(rhs); + return *this; +} +``` + +> C++11 move + +我们在big three上加上move ctor与move assignment就构成了big five。 + +此时再次拓展上述的代码: + +```cpp +// move ctor +smart_ptr(smart_ptr &&rhs) noexcept { + std::cout << "move ctor" << std::endl; + ptr_ = rhs.ptr_; + if (ptr_) + rhs.ptr_ = nullptr; +} + +// move assignment +smart_ptr &operator=(smart_ptr &&rhs) noexcept { + std::cout << "move assignment" << std::endl; + smart_ptr tmp(rhs); + swap(rhs); + return *this; +} +``` + +**实际上,我们比那个不需要多写代码move assignment,copy-and-swap 技巧 和 move-and-swap 技巧是共享同一个函数的。**当copy构造为上述的方法4时,**对于C++ 11,编译器会依据参数是左值还是右值在拷贝构造函数和移动构造函数间进行选择:** + +```cpp +smart_ptr &operator=(smart_ptr rhs) noexcept { + swap(rhs); + return *this; +} +``` + +所以当这个同上述写的 + +```cpp +smart_ptr &operator=(smart_ptr &&rhs) noexcept{} +``` + +同时存在,就会出现error: ambiguous overload for ‘operator=’ 。 + +调用处如下: + +```cpp +A::smart_ptr s1("hello"), s2("world"); +A::smart_ptr s3 = s1; +A::smart_ptr s4 = std::move(s1); +``` + +- 如果是 s3 = s1,这样就会调用拷贝构造函数来初始化other(因为s1是左值),赋值操作符会与新创建的对象交换数据,深度拷贝。这就是copy and swap 惯用法的定义:构造一个副本,与副本交换数据,并让副本在作用域内自动销毁。 +- 如果是s4 = std::move(s1),这样就会调用移动构造函数来初始化rhs(因为std::move(s1)是右值),所以这里没有深度拷贝,只有高效的数据转移。 + +因此也可以称呼它为“统一赋值操作符”,因为它合并了"拷贝赋值"与"移动赋值"。 \ No newline at end of file diff --git a/codingStyleIdioms/enumclass/README.md b/codingStyleIdioms/enumclass/README.md new file mode 100644 index 0000000..b94228b --- /dev/null +++ b/codingStyleIdioms/enumclass/README.md @@ -0,0 +1,73 @@ +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/enum-class/) 同步生成,网站版本含多语言与更新排版。 + +# C++ enum class 强类型枚举 + +在Effective modern C++中Item 10: Prefer scoped enums to unscoped enum,调到要用有范围的enum class代替无范围的enum。 + +例如: + +```cpp +enum Shape {circle,retangle}; +auto circle = 10; // error +``` + +上述错误是因为两个circle在同一范围。 +对于enum等价于: +```cpp +#define circle 0 +#define retangle 1 +``` +因此后面再去定义circle就会出错。 + +所以不管枚举名是否一样,里面的成员只要有一致的,就会出问题。 +例如: +```cpp +enum A {a,b}; +enum B {c,a}; +``` +a出现两次,在enum B的a处报错。 + +根据前面我们知道,enum名在范围方面没有什么作用,因此我们想到了namespace,如下例子: +```cpp +// 在创建枚举时,将它们放在名称空间中,以便可以使用有意义的名称访问它们: +namespace EntityType { + enum Enum { + Ground = 0, + Human, + Aerial, + Total + }; +} + +void foo(EntityType::Enum entityType) +{ + if (entityType == EntityType::Ground) { + /*code*/ + } +} +``` +将命名空间起的有意思点,就可以达到想要的效果。 + +但是不断的使用命名空间,势必太繁琐,而且如果我不想使用namespace,要达到这样的效果,便会变得不安全,也没有约束。 + +因此在c++11后,引入enum class。 + +enum class 解决了为enum成员定义类型、类型安全、约束等问题。 +回到上述例子: +```cpp +// enum class +enum class EntityType { + Ground = 0, + Human, + Aerial, + Total +}; + +void foo(EntityType entityType) +{ + if (entityType == EntityType::Ground) { + /*code*/ + } +} +``` +这便是这一节要阐述的惯用法:enum class。 \ No newline at end of file diff --git a/codingStyleIdioms/pImpl/README.md b/codingStyleIdioms/pImpl/README.md new file mode 100644 index 0000000..c87e6ce --- /dev/null +++ b/codingStyleIdioms/pImpl/README.md @@ -0,0 +1,59 @@ +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/pimpl/) 同步生成,网站版本含多语言与更新排版。 + +# C++ Pimpl 惯用法 + +“指向实现的指针”或“pImpl”是一种 C++ 编程技巧,它将类的实现细节从对象表示中移除,放到一个分离的类中,并以一个不透明的指针进行访问。 + +使用pImpl惯用法的原因如下: + +考虑如下例子: + +```cpp +class X +{ +private: + C c; + D d; +} ; +``` + +变成pImpl就是下面这样子 + +```cpp +class X +{ +private: + struct XImpl; + XImpl* pImpl; +}; +``` + +CPP定义: + +```cpp +struct X::XImpl +{ + C c; + D d; +}; +``` + +- 二进制兼容性 + +开发库时,可以在不破坏与客户端的二进制兼容性的情况下向XImpl添加/修改字段(这将导致崩溃!)。 由于在向Ximpl类添加新字段时X类的二进制布局不会更改,因此可以安全地在次要版本更新中向库添加新功能。 + +当然,您也可以在不破坏二进制兼容性的情况下向X / XImpl添加新的公共/私有非虚拟方法,但这与标准的标头/实现技术相当。 + +- 数据隐藏 + +如果您正在开发一个库,尤其是专有库,则可能不希望公开用于实现库公共接口的其他库/实现技术。 要么是由于知识产权问题,要么是因为您认为用户可能会被诱使对实现进行危险的假设,或者只是通过使用可怕的转换技巧来破坏封装。 PIMPL解决/缓解了这一难题。 + +- 编译时间 + +编译时间减少了,因为当您向XImpl类添加/删除字段和/或方法时(仅映射到标准技术中添加私有字段/方法的情况),仅需要重建X的源(实现)文件。 实际上,这是一种常见的操作。 + +使用标准的标头/实现技术(没有PIMPL),当您向X添加新字段时,曾经重新分配X(在堆栈或堆上)的每个客户端都需要重新编译,因为它必须调整分配的大小 。 好吧,每个从未分配X的客户端也都需要重新编译,但这只是开销(客户端上的结果代码是相同的)。 + + + +> https://stackoverflow.com/questions/8972588/is-the-pimpl-idiom-really-used-in-practices \ No newline at end of file diff --git a/concurrency/Threading_In_CPlusPlus/README.md b/concurrency/Threading_In_CPlusPlus/README.md new file mode 100644 index 0000000..4d2cafc --- /dev/null +++ b/concurrency/Threading_In_CPlusPlus/README.md @@ -0,0 +1,252 @@ +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-concurrency/threading-in-cpp/) 同步生成,网站版本含多语言与更新排版。 + +# C++ 多线程入门 + +[TOC] + +本文中的所有代码见《C++那些事》仓库。 + +> https://github.com/Light-City/CPlusPlusThings + +## 1.理解线程与进程 + +线程是调度的基本单位 进程是资源分配的基本单位。可以把一个程序理解为进程,进程又包含多个线程。 + +例如:浏览器是个进程,而每开一个tab就是一个线程。 + +两者简单区别: + +- **地址空间和其它资源**:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。 +- **通信**: 进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。 +- **调度和切换**:线程上下文切换比进程上下文切换要快得多。 +- 在**多线程OS**中,进程不是一个可执行的实体。 + +至于IPC通信与线程通信后面会新开一篇文章。 + +## 2.五种创建线程的方式 + +- 函数指针 +- Lambda函数吧 +- Functor(仿函数) +- 非静态成员函数 +- 静态成员函数 + +### 2.1 函数指针 + +```cpp +// 1.函数指针 +void fun(int x) { + while (x-- > 0) { + cout << x << endl; + } +} +// 调用 +std::thread t1(fun, 10); +t1.join(); +``` + +### 2.2 Lambda函数 + +```cpp +// 注意:如果我们创建多线程 并不会保证哪一个先开始 +int main() { + // 2.Lambda函数 + auto fun = [](int x) { + while (x-- > 0) { + cout << x << endl; + } + }; +// std::1.thread t1(fun, 10); + // 也可以写成下面: + std::thread t1_1([](int x) { + while (x-- > 0) { + cout << x << endl; + } + }, 11); +// std::1.thread t2(fun, 10); +// t1.join(); + t1_1.join(); +// t2.join(); + return 0; +} +``` + +### 2.3 仿函数 + +```cpp +// 3.functor (Funciton Object) +class Base { +public: + void operator()(int x) { + while (x-- > 0) { + cout << x << endl; + } + } +}; +// 调用 +thread t(Base(), 10); +t.join(); +``` + +### 2.4 非静态成员函数 + +```cpp +// 4.Non-static member function +class Base { +public: + void fun(int x) { + while (x-- > 0) { + cout << x << endl; + } + } +}; +// 调用 +thread t(&Base::fun,&b, 10); +t.join(); +``` + +### 2.5 静态成员函数 + +```cpp +// 4.Non-static member function +class Base { +public: + static void fun(int x) { + while (x-- > 0) { + cout << x << endl; + } + } +}; +// 调用 +thread t(&Base::fun, 10); +t.join(); +``` + +## 3.join与detach + +### 3.1 join + +- 一旦线程开始,我们要想等待线程完成,需要在该对象上调用join() +- 双重join将导致程序终止 +- 在join之前我们应该检查显示是否可以被join,通过使用joinable() + +```cpp +void run(int count) { + while (count-- > 0) { + cout << count << endl; + } + std::this_thread::sleep_for(chrono::seconds(3)); +} + +int main() { + thread t1(run, 10); + cout << "main()" << endl; + t1.join(); + if (t1.joinable()) { + t1.join(); + } + cout << "main() after" << endl; + return 0; +} +``` + +### 3.2 detach + +- 这用于从父线程分离新创建的线程 +- 在分离线程之前,请务必检查它是否可以joinable,否则可能会导致两次分离,并且双重detach()将导致程序终止 +- 如果我们有分离的线程并且main函数正在返回,那么分离的线程执行将被挂起 + +```cpp +void run(int count) { + while (count-- > 0) { + cout << count << endl; + } + std::this_thread::sleep_for(chrono::seconds(3)); +} + +int main() { + thread t1(run, 10); + cout << "main()" << endl; + t1.detach(); + if(t1.joinable()) + t1.detach(); + cout << "main() after" << endl; + return 0; + +``` + +## 4.临界区与互斥量 + +### 4.1 什么是临界区(Critical Sections)? + +临界段是一段代码,如果要使程序正确运行,一次只能由一个线程执行。如果两个线程(或进程)同时执行临界区内的代码,则程序可能不再具有正确的行为。 + +### 4.2 只是增加一个变量是临界区吗? + +可能是吧。 + +增加变量(i ++)的过程分三个步骤: + +- 将内存内容复制到CPU寄存器。 load +- 在CPU中增加该值。 increment +- 将新值存储在内存中。 store + +如果只能通过一个线程访问该内存位置(例如下面的变量i),则不会出现争用情况,也没有与i关联的临界区。 但是sum变量是一个全局变量,可以通过两个线程进行访问。 两个线程可能会尝试同时增加变量。 + +```cpp +#include +#include +#include + +using namespace std; + +int sum = 0; //shared + +mutex m; + +void *countgold() { + int i; //local to each thread + for (i = 0; i < 10000000; i++) { + sum += 1; + } + return NULL; +} + +int main() { + thread t1(countgold); + thread t2(countgold); + + //Wait for both threads to finish + t1.join(); + t2.join(); + + cout << "sum = " << sum << endl; + return 0; +} +``` + +上面代码的典型输出是sum总和为20000000。由于存在竞争条件,每次运行程序都会打印不同的总和。 该代码不会阻止两个线程同时读写总和。 例如,两个线程都将sum的当前值复制到运行每个线程的CPU中(让我们选择123)。 两个线程都将一个递增到自己的副本。 两个线程都写回该值(124)。 如果线程在不同时间访问了总和,则计数将为125。 + +### 4.3 如何确保一次只有一个线程可以访问全局变量? + +如果一个线程当前处于临界区,我们希望另一个线程等待,直到第一个线程完成。为此,我们可以使用**互斥锁(**互斥的缩写)。 + +互斥锁形象比喻: + +![](http://www.ruanyifeng.com/blogimg/asset/201304/bg2013042408.jpg) + +一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫["互斥锁"](http://zh.wikipedia.org/wiki/互斥锁)(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。 + +```cpp +m.lock(); +sum += 1; +m.unlock(); +``` + +上述代码就可以正常输出:`sum = 20000000`。 + +## 参考资料 + +> http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html?utm_source=com.ideashower.readitlater.pro&utm_medium=social&utm_oi=35626384621568 + +> https://www.youtube.com/watch?v=eZ8yKZo-PGw&list=PLk6CEY9XxSIAeK-EAh3hB4fgNvYkYmghp&index=4 \ No newline at end of file diff --git a/concurrency/concurrency_v1/README.md b/concurrency/concurrency_v1/README.md new file mode 100644 index 0000000..2a05db9 --- /dev/null +++ b/concurrency/concurrency_v1/README.md @@ -0,0 +1,9 @@ +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-concurrency/cpp-concurrency-in-action/) 同步生成,网站版本含多语言与更新排版。 + +# C++ Concurrency in Action 导读 + +- [第一章](https://github.com/Light-City/CPlusPlusThings/tree/master/concurrency/concurrency_v1/chapter1) + +- [第二章](https://github.com/Light-City/CPlusPlusThings/tree/master/concurrency/concurrency_v1/chapter2) + +学习资料(第二版中文笔记):[https://downdemo.gitbook.io/cpp-concurrency-in-action-2ed/](https://downdemo.gitbook.io/cpp-concurrency-in-action-2ed/) \ No newline at end of file diff --git a/cpp2.0/cpp11/README.md b/cpp2.0/cpp11/README.md index e77f87b..422e410 100644 --- a/cpp2.0/cpp11/README.md +++ b/cpp2.0/cpp11/README.md @@ -1,8 +1,10 @@ -# C++新特性 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp11/cpp11/) 同步生成,网站版本含多语言与更新排版。 + +# C++11 新特性概览 ## 1.C++11新特性 -- [Variadic Templates](./variadic) +- [Variadic Templates](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/variadic) - Spaces in Template Expressions @@ -11,17 +13,17 @@ vector> // before c++ 11 error error: ‘>>’ should be ‘> >’ within a nested template argument list,c++11后可以正常通过 ``` -- [nullptr and nullptr_t](./nullptr.cpp) +- [nullptr and nullptr_t](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/nullptr.cpp) -- [Automatic Type Deduction with auto](./auto.cpp) +- [Automatic Type Deduction with auto](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/auto.cpp) -- [Uniform Initialization ](./uniform_initialization.cpp) +- [Uniform Initialization ](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/uniform_initialization.cpp) -- [initializer_list](./initializer.cpp) +- [initializer_list](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/initializer.cpp) -- [explicit for ctors taking more than one argument](./explicit.cpp) +- [explicit for ctors taking more than one argument](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/explicit.cpp) -- [range-based for statement](./auto.cpp) +- [range-based for statement](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/auto.cpp) ```cpp for(decl:col) { @@ -29,33 +31,33 @@ } ``` -- [=default,=delete](./default_delete.cpp) +- [=default,=delete](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/default_delete.cpp) 如果你自行定义了一个ctor,那么编译器就不会给你一个default ctor 如果强制加上=default,就可以重新获得并使用default ctor. - Alias(化名)Template(template typedef) - - [alias.cpp](./alias.cpp) - - [template_template.cpp](./template_template.cpp) + - [alias.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/alias.cpp) + - [template_template.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/template_template.cpp) -- [template template parameter](./template_template.cpp) +- [template template parameter](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/template_template.cpp) -- [type alias](./type_alias.cpp) +- [type alias](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/type_alias.cpp) -- [noexcept](./noexcept.cpp) +- [noexcept](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/noexcept.cpp) -- [override](./override.cpp) +- [override](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/override.cpp) -- [final](./final.cpp) +- [final](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/final.cpp) -- [decltype](./decltype.cpp) +- [decltype](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/decltype.cpp) -- [lambda](./lambda.cpp) +- [lambda](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/lambda.cpp) -- [Rvalue reference](./rvalue.cpp) +- [Rvalue reference](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/rvalue.cpp) -- [move aware class](./move.cpp) +- [move aware class](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/move.cpp) - 容器-结构与分类 @@ -63,8 +65,8 @@ - (2) 关联式容器包括:set/multiset,map/multimap - (3) 无序容器(C++2.0新引入,更换原先hash_xxx为unordered_xxx)包括:unordered_map/unordered_multimap,unordered_set/unordered_multiset -- [Hash Function](./hash.cpp) +- [Hash Function](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/hash.cpp) -- [tuple](./tuple.cpp) +- [tuple](https://github.com/Light-City/CPlusPlusThings/tree/master/cpp2.0/cpp11/tuple.cpp) 学习资料:https://www.bilibili.com/video/av51863195?from=search&seid=3610634846288253061 \ No newline at end of file diff --git a/design_pattern/singleton/README.md b/design_pattern/singleton/README.md index 794f013..89d3441 100644 --- a/design_pattern/singleton/README.md +++ b/design_pattern/singleton/README.md @@ -1,4 +1,6 @@ -## C++设计模式之单例模式 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/design-patterns/singleton-pattern/) 同步生成,网站版本含多语言与更新排版。 + +# C++ 单例模式 ## 0.导语 @@ -325,4 +327,4 @@ public: > https://www.cnblogs.com/liyuan989/p/4264889.html -> https://segmentfault.com/a/1190000015950693 +> https://segmentfault.com/a/1190000015950693 \ No newline at end of file diff --git a/design_pattern/singleton/singleton_default.hpp b/design_pattern/singleton/singleton_default.hpp new file mode 100644 index 0000000..ac5a32c --- /dev/null +++ b/design_pattern/singleton/singleton_default.hpp @@ -0,0 +1,58 @@ +/** + * @file singleton_default.hpp + * @brief 可复用的线程安全单例模板基类(CRTP) + * 受 issue #303 启发:https://github.com/Light-City/CPlusPlusThings/issues/303 + * 参考 boost::container::singleton_default 的思路。 + * + * 用法: + * class MyConfig : public SingletonDefault { + * friend class SingletonDefault; + * private: + * MyConfig() = default; // 构造私有,交由基类创建 + * public: + * int value = 42; + * }; + * MyConfig::instance().value = 1; + * + * 编译:g++ -std=c++11 main.cpp + * @author 光城 + * @version v1 + * @date 2026-10 + */ + +#pragma once + +#include + +template +class SingletonDefault { +public: + SingletonDefault(const SingletonDefault&) = delete; + SingletonDefault& operator=(const SingletonDefault&) = delete; + + static T& instance() { + // C++11 起,局部静态变量的初始化由标准保证线程安全("magic statics") + static T inst; + return inst; + } + +protected: + SingletonDefault() = default; + ~SingletonDefault() = default; +}; + +/** + * 若需要 C++03 或对 magic statics 不放心的环境,可用 call_once 版本: + * + * template + * class SingletonDefaultOnce { + * public: + * static T& instance() { + * std::call_once(once_, [] { inst_.reset(new T); }); + * return *inst_; + * } + * private: + * static std::once_flag once_; + * static std::unique_ptr inst_; + * }; + */ diff --git a/extension/some_problem/README.md b/extension/some_problem/README.md new file mode 100644 index 0000000..d99e1df --- /dev/null +++ b/extension/some_problem/README.md @@ -0,0 +1,100 @@ +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/cpp-string-to-int/) 同步生成,网站版本含多语言与更新排版。 + +# C++ string 转 int + +个人公众号: + +![](https://raw.githubusercontent.com/Light-City/CPlusPlusThings/master/img/wechat.jpg) + +首先提出解决方案: + +- atoi +- strtol +- stoi + +这几个有什么不同呢?下面测试对比。 + +> C语言风格函数 + +atoi与strtol对比: + +```cpp +string str = "16s"; +int a = atoi(str.c_str()); +int b = strtol(str.c_str(), nullptr, 10); +``` + +输出: + +``` +atoi的结果为:16 +strtol的结果为:16 +``` + +这两个函数都是从字符串开始寻找数字或者正负号或者小数点,遇到非法字符终止。 + +所以到上述s字符就不输出了,提前结束,也就是说当你的字符串不是数字的时候,或者小数点等非数字,不会报异常!直接输出0. + +例如: + +```cpp +string str = "asdsa"; +int a = atoi(str.c_str()); +int b = strtol(str.c_str(), nullptr, 10); +``` + +输出: + +``` +0 +0 +``` + +strtol相比与atoi来说,支持多种进制转换,例如8进制等 + +例如: + +``` +int b = strtol(str.c_str(), nullptr, 8); +``` + +> C++风格 + +在C++中可以使用stoi来转int,这个函数相比于前两个一个最大特点是:异常! + +我们知道C++相比于C语言多了异常,这也是这个函数在C++中具有的最显著功能。 + +例如: + +```cpp +string str1 = "asq,"; +// int c = stoi(str1); // 报异常 +string str2 = "12312"; +int c = stoi(str2); // ok +cout << c << endl; +``` + +异常如下: + +``` +terminate called after throwing an instance of 'std::invalid_argument' +what(): stoi +``` + +> 自定义 + +也就是自己写呗,如下: + +```cpp +int stringToInt(const string &s) { + int v; + stringstream ss; + ss << s; + ss >> v; + return v; +} +int main() { + int i = stringToInt("2.3"); + cout< 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/cpp-courses/modern-cpp-30-geektime/) 同步生成,网站版本含多语言与更新排版。 + +# 极客时间《现代 C++ 实战 30 讲》 + +- [堆、栈、RAII:C++里该如何管理资源?](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/RAII) + - [堆](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/RAII/heap.cpp) + - [栈](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/RAII/stack.cpp) + - [RAII](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/RAII/RAII.cpp) +- [自己动手,实现C++的智能指针](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/smart_ptr) + - [auto_ptr、scope_ptr](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/smart_ptr/auto_scope.cpp) + - [unique_ptr](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/smart_ptr/unique_ptr.cpp) + - [shared_ptr](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/smart_ptr/shared_ptr.cpp) +- [右值和移动究竟解决了什么问题?](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/reference) + - [左值与右值](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/reference/reference.cpp) + - [延长声明周期](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/reference/lifetime.cpp) + - [引用折叠](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/reference/collapses.cpp) + - [完美转发](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/reference/forward.cpp) + - [不要返回本地变量的引用](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/reference/don'treturnReference.cpp) +- [容器1](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/container1) +- [容器2](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/container2) +- [异常](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/exception) +- [字面量、静态断言和成员函数说明符](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/literalAssert) +- [是不是应该返回对象?](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/obj) +- [编译期多态:泛型编程和模板入门](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/compilerpoly) +- [译期能做些什么?一个完整的计算世界](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/compilercompute) +- [SFINAE:不是错误的替换失败是怎么回事?](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/SFINAE) +- [constexpr:一个常态的世界](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/constexpr) +- [函数对象和lambda:进入函数式编程](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/functionLambda) +- [内存模型和atomic:理解并发的复杂性](https://github.com/Light-City/CPlusPlusThings/tree/master/learn_class/modern_cpp_30/memorymodel_atomic) \ No newline at end of file diff --git a/practical_exercises/10_day_practice/readme.md b/practical_exercises/10_day_practice/readme.md index 8bf2c38..110c16a 100644 --- a/practical_exercises/10_day_practice/readme.md +++ b/practical_exercises/10_day_practice/readme.md @@ -1,41 +1,62 @@ -# 10日c++实战狂练 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/practical-exercises/10-day-cpp-crash-course/) 同步生成,网站版本含多语言与更新排版。 -- [x] [day1](day1) +# C++ 十日速成练习 + +- [x] [day1](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/10_day_practice/day1) 基本c++语法 -- [x] [day2](day2) +- [x] [day2](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/10_day_practice/day2) 递归、结构体、枚举、静态变量等 -- [x] [day3](day3) +- [x] [day3](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/10_day_practice/day3) 函数 -- [x] [day4](day4) +- [x] [day4](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/10_day_practice/day4) 函数深入 -- [x] [day5](day5) +- [x] [day5](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/10_day_practice/day5) 继承多态 -- [x] [day6](day6) +- [x] [day6](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/10_day_practice/day6) 虚函数、抽象类 -- [x] [day7](day7) +- [x] [day7](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/10_day_practice/day7) 运算符重载 -- [x] [day8](day8) +- [x] [day8](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/10_day_practice/day8) 模板与STL -- [x] [day9](day9) +- [x] [day9](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/10_day_practice/day9) 异常 -- [x] [day10](day10) +- [x] [day10](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/10_day_practice/day10) -文件与流 \ No newline at end of file +文件与流 + +## 每日题目说明(回应 [issue #136](https://github.com/Light-City/CPlusPlusThings/issues/136)) + +题目即每天的**文件名 + 文件内注释**,这里给出汇总清单,练习时先看题目名,自己写一遍,再对照仓库实现: + +| Day | 主题 | 题目(对应仓库文件) | +| --- | --- | --- | +| 1 | 基本语法 | 注释对齐(annotate)、打印输出(print)、闰年判断(runnian)、union 初探(union) | +| 2 | 递归/结构体/枚举 | 汉诺塔(hanoi)、幂运算(pow)、递归基础(rec1)、简单计算器(compute)、枚举应用(enum) | +| 3 | 函数 | 函数重载、默认参数、值/引用/指针传参对比 | +| 4 | 函数深入 | 函数指针、回调、可变参数、内联函数 | +| 5 | 类与对象 | 构造/析构(ctor_dtor)、继承访问控制(inherit_access)、运算符重载(rule)、组合关系(rela)、虚函数初探(virtual) | +| 6 | 继承多态 | 虚函数、抽象类、纯虚函数、多态综合案例 | +| 7 | 运算符重载 | 算术/关系/下标/圆括号重载、强制转换运算符 | +| 8 | 模板与 STL | 类模板(class_template)、函数模板(func)、STL 容器入门(stl) | +| 9 | 异常 | try/catch、自定义异常、异常安全 | +| 10 | 文件与流 | 文件读写、流的格式化 | + +> 建议:每道题先自己从零写,写不出来再看实现;写完了对比差距,收获最大(参考 [issue #180](https://github.com/Light-City/CPlusPlusThings/issues/180) 的讨论)。 diff --git a/practical_exercises/key_exercises/README.md b/practical_exercises/key_exercises/README.md index eebad50..6cf6579 100644 --- a/practical_exercises/key_exercises/README.md +++ b/practical_exercises/key_exercises/README.md @@ -1,26 +1,27 @@ -## 重点实战练习 +> 本文档由 [C++那些事网站](https://light-city.github.io/stories_things/practical-exercises/key-cpp-exercises/) 同步生成,网站版本含多语言与更新排版。 -├── [中括号重载.cpp](bracket_overloading.cpp) -├── [时钟++运算符重载.cpp](clock.cpp) -├── [运算符重载之强制转换.cpp](operator_cast.cpp) -└── [重载圆括号的时钟.cpp](operator_circle.cpp) +# C++ 重点实战练习 -├── [函数模板.cpp](func_temp.cpp) +├── [中括号重载.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/bracket_overloading.cpp) +├── [时钟++运算符重载.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/clock.cpp) +├── [运算符重载之强制转换.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/operator_cast.cpp) +└── [重载圆括号的时钟.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/operator_circle.cpp) -├── [动态数组.cpp](array.cpp) +├── [函数模板.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/func_temp.cpp) -├── [字典插入与查找.cpp](map_insert_look.cpp) +├── [动态数组.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/array.cpp) -├── [异常捕获.cpp](try.cpp) +├── [字典插入与查找.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/map_insert_look.cpp) -├── [类模板之栈.cpp](stack.cpp) +├── [异常捕获.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/try.cpp) -├── [类模板特化之数组.cpp](array_template.cpp) +├── [类模板之栈.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/stack.cpp) -├── [继承与封装.cpp](override.cpp) +├── [类模板特化之数组.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/array_template.cpp) -├── [读写文件综合题.cpp](read_file.cpp) -├── [输入输出运算符重载.cpp](io_operator_overload.cpp) -├── [输入输出重载.cpp](io_operator.cpp) -├── [输出格式.cpp](output.cpp) +├── [继承与封装.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/override.cpp) +├── [读写文件综合题.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/read_file.cpp) +├── [输入输出运算符重载.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/io_operator_overload.cpp) +├── [输入输出重载.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/io_operator.cpp) +├── [输出格式.cpp](https://github.com/Light-City/CPlusPlusThings/tree/master/practical_exercises/key_exercises/output.cpp) \ No newline at end of file diff --git a/site_extras/about.md b/site_extras/about.md new file mode 100644 index 0000000..c422934 --- /dev/null +++ b/site_extras/about.md @@ -0,0 +1,59 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/about/) 的文章,由站点同步脚本导出。 + +# 联系作者 + +我是开源社区的积极参与者,为[CPlusPlusThings](https://github.com/Light-City/CPlusPlusThings)、[Apache Arrow](https://github.com/apache/arrow)和[DuckDB](https://github.com/duckdb/duckdb)等项目做出了贡献。我热衷于促进开源计划的发展并支持协作生态系统。 + +我是一名全栈工程师。 + +我❤️后端工程师。 + +🖖 我主要将精力放在使用 C/Go/C++ 进行后端开发上。 + +我喜欢在以下平台写文章: + +- [Medium](https://light-city.medium.com/) +- [WeChat](https://mp.weixin.qq.com/s/5_Ms8bGbA-PWj5EwmTBaQA) +- [ZhiHu](https://www.zhihu.com/people/francis-52-23/posts) +- [BiliBili](https://space.bilibili.com/367439608?spm_id_from=333.337.0.0) + +现在专注于数据库相关的工作,欢迎与我交流! + +- 微信 + +搜索:gcxzhang + +- 微信公众号 + + + + + + + + +
+
+ 知识星球 +
+
+ 微信公众号 +
+ +- 赞赏我 + + + + + + + +
+
+ 支付宝 +
+
+ 微信 +
+ +- 邮箱: 455954986@qq.com diff --git a/site_extras/cplusplus-things-overview.md b/site_extras/cplusplus-things-overview.md new file mode 100644 index 0000000..cd6a7a6 --- /dev/null +++ b/site_extras/cplusplus-things-overview.md @@ -0,0 +1,146 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cplusplus-things-overview/) 的文章,由站点同步脚本导出。 + +# 项目概要 + +## 1. 项目概述 + +- 中文名:[**C++那些事**](https://github.com/Light-City/CPlusPlusThings) +- 英文名:[**Stories About C Plus Plus or CPlusPlusThings**](https://github.com/Light-City/CPlusPlusThings) + +**C++ 那些事** 是一个面向 C++ 初学者和进阶开发者的开源学习项目。该项目旨在帮助学习者从 C++ 的基础入门到高级进阶,涵盖从语法基础、面向对象编程、模板编程、STL 使用,到多线程并发、内存管理、性能优化等高级主题。此外,项目还深入分析了一些经典的开源库和框架的源码,帮助开发者更好地理解 C++ 在实际项目中的应用。 + +## 2. 项目目标 + +- **入门到进阶**:为初学者提供一个系统化的学习路径,帮助他们从零基础逐步掌握 C++ 的核心概念和高级特性。 +- **面试准备**:为求职者提供常见 C++ 面试题的解析和实战演练,帮助他们在面试中脱颖而出。 +- **源码分析**:通过分析经典开源项目的源码,帮助开发者深入理解 C++ 的设计模式和实现原理。 +- **多线程与并发**:深入探讨 C++ 中的多线程编程和并发控制,帮助开发者编写高效、安全的并发程序。 +- **性能优化**:介绍 C++ 中的性能优化技巧,帮助开发者编写高效的代码。 + +## 3. 项目内容 + +**C++ 那些事** 项目内容丰富,涵盖了 C++ 的各个方面,主要包括以下几个模块: + +- **基础进阶** + + - **关键字与特性**:如 `const`、`static`、`this`、`inline`、`sizeof`、`virtual`、`volatile`、`explicit`、`friend`、`using`、`::`、`enum`、`decltype` 等关键字的详细解析。 + - **函数与指针**:函数指针、纯虚函数和抽象类、`vptr_vtable` 等。 + - **数据结构**:`struct`、`union`、位域、引用与指针的区别等。 + - **C 实现 C++ 多态**:通过 C 语言实现 C++ 的多态特性。 + - **宏与断言**:宏的定义与使用、`assert` 的使用场景。 + +- **实战系列** + + - **10 日狂练**:通过 10 天的密集练习,快速提升 C++ 编程能力。 + - **重点实战练习**:针对 C++ 中的重点难点进行实战演练,巩固所学知识。 + +- **STL 源码剖析** + + - **容器**:`array`、`deque`、`queue`、`stack`、`list`、`vector`、`rb_tree`、`set`、`multiset`、`map`、`multimap`、`hashtable`、`unordered_map` 等容器的源码分析。 + - **迭代器与特性**:`iterator`、`traits`、`typename` 等。 + - **EBO 优化**:STL 设计中的空基类优化(EBO)技术。 + +- **C++2.0 新特性** + + - **C++11 新特性**:如自动类型推导、Lambda 表达式、右值引用、智能指针等;另见站内 [C++11 专题](modern-cpp/cpp11/index.md)。 + - **C++17 / C++20**:[C++17 专题](modern-cpp/cpp17/index.md)、[C++20 专题](modern-cpp/cpp20/index.md)。 + +- **拓展专题** + + - **语言机制深入**:mutable、const 全家桶、ABI、尾递归等(见导航「基础进阶 → 语言机制深入」)。 + - **内存与调试**、**性能测评**、**项目实战**、**阅读与技巧**:独立导航栏目,含 AddressSanitizer、Benchmark、pybind11、gRPC 等实战文。 + +- **设计模式** + + - **常见设计模式**:如单例模式、工厂模式、观察者模式等在 C++ 中的应用。 + +- **并发编程** + + - **多线程与多进程**:`std::thread`、`std::async`、`std::mutex`、`std::condition_variable` 等多线程编程技术。 + - **C++ Concurrency in Action**:深入探讨 C++ 中的并发编程模型与实践。 + +- **C++ 惯用法** + + - **编程风格与惯用法**:探讨 C++ 中的常见编程风格与惯用法,帮助开发者写出更优雅的代码。 + +- **学习课程** + + - **极客时间《现代 C++ 实战 30 讲》**:推荐学习资源,帮助开发者系统学习现代 C++。 + +- **工具篇** + + - **调试与输出工具**:如 `dbg-macro`、`rr` 等调试工具的使用。 + - **容器快捷输出工具**:简化容器输出的工具。 + - **观察编译过程**:通过工具观察 C++ 代码的编译过程变化。 + +## 4. 项目特色 + +- **系统化学习路径**:从基础到进阶,内容循序渐进,适合不同层次的学习者。 +- **理论与实践结合**:不仅有理论知识的讲解,还有大量的代码示例和实战项目。 +- **源码深度分析**:通过分析经典开源项目的源码,帮助开发者深入理解 C++ 的设计与实现。 +- **多线程与并发**:深入探讨 C++ 中的并发编程,帮助开发者编写高效、安全的并发程序。 +- **面试导向**:提供常见面试题的解析,帮助求职者更好地准备面试。 + +## 5. 项目资源 + +- **在线学习网站**:[C++那些事](https://light-city.github.io/stories_things/) +- **微信公众号**:[光城](https://mp.weixin.qq.com/s/5_Ms8bGbA-PWj5EwmTBaQA) +- **B站视频**:[未来的光城](https://www.bilibili.com/video/BV1Rv4y1H7LB/?vd_source=bb6532dcd5b1d6b26125da900adb618e),项目内容同步至 B站,以视频形式展示,方便学习者观看。 + +## 6. 参与方式 + +[**C++那些事**](https://github.com/Light-City/CPlusPlusThings)是一个开源项目,欢迎各位开发者参与贡献。你可以通过以下方式参与: + +- **Star 和 Fork**:如果你觉得项目对你有帮助,欢迎在 GitHub 上 Star 和 Fork 项目。 +- **提交 PR**:如果你有好的内容或改进建议,欢迎提交 Pull Request。 +- **反馈与建议**:如果你在使用过程中遇到问题或有任何建议,欢迎在项目的 Issue 区提出。 + +## 7. 项目未来规划 + +- **扩展更多高级主题**:如 C++20 新特性、元编程、编译期计算等。 +- **增加更多实战项目**:通过更多的实战项目,帮助开发者将所学知识应用到实际开发中。 +- **社区建设**:建立更活跃的社区,鼓励开发者之间的交流与分享。 + +## 8. 结语 + +[**C++那些事**](https://github.com/Light-City/CPlusPlusThings)是一个致力于帮助 C++ 学习者从入门到进阶的开源项目。无论你是初学者还是有一定经验的开发者,都可以在这里找到适合自己的学习资源。希望通过这个项目,能够帮助更多的人掌握 C++,并在实际开发中运用自如。 + +欢迎各位开发者 Star、Fork、PR,共同推动 C++ 社区的发展! + +## 9. 关于作者 + +- **微信**:搜索 `gcxzhang` + +- **微信公众号** + + + + + + + + +
+
+ 知识星球 +
+
+ 微信公众号 +
+ +- **赞赏支持** + + + + + + + + +
+
+ 支付宝 +
+
+ 微信 +
\ No newline at end of file diff --git a/site_extras/cpp-benchmarks-cpp-sso-benchmark.md b/site_extras/cpp-benchmarks-cpp-sso-benchmark.md new file mode 100644 index 0000000..8d4c9fc --- /dev/null +++ b/site_extras/cpp-benchmarks-cpp-sso-benchmark.md @@ -0,0 +1,121 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-benchmarks/cpp-sso-benchmark/) 的文章,由站点同步脚本导出。 + +# C++ string SSO 性能测试 + +今天想问大家两个问题: + +**1.下面代码性能上有没有区别?** + +```cpp +std::string s1("hello world hel"); +std::string s2("hello world helo"); +``` + +**2.sizeof(std::string)是多少?** + +下面,我们来一起探索这两个问题。 + +由于这两个答案在g++(libstdc++)与clang(libc++)上的结果不一样,所以这里会分别进行分析。 + +## 1.g++(libstdc++) + +g++的源码我们以g++-10为例,详细代码可以戳这个github地址,这里列出比较核心的代码。 + +> https://github.com/gcc-mirror/gcc/blob/devel/omp/gcc-10/libstdc%2B%2B-v3/include/bits/basic_string.h#L171 + +```cpp +template +class basic_string { + private: + struct _Alloc_hider : allocator_type { + pointer _M_p; // The actual data. + }; + enum { __min_cap = 15 / sizeof(value_type) }; + _Alloc_hider _M_dataplus; // 8 bytes + size_type __size_; // 8 bytes + + union { + _CharT _M_local_buf[_S_local_capacity + 1]; // 16 bytes + size_type _M_allocated_capacity; // 8 bytes + }; // 16 bytes +}; // 32 bytes +``` + +在上面代码中注释部分详细的列出了string占据的字节(注意:这里的value_type以char来计算,也就是sizeof(value_type) = 1)。 + +sizeof(string) = 8 + 8 + 16 = 32。 + +其中8字节ptr,8字节size,16字节union。 + +性能层面,这里实现了SSO,本地buffer会存储最多15个字符,当超过15字符便会分配在heap上,这里我写了一个benchmark来对比性能。 + +- 短字符串测试内容为:"hello world hel",总共15个字符。 +- 长字符串测试内容为:"hello world helo",总共为16个字符。 + +``` +std::string s1("hello world hel"); +std::string s2("hello world helo"); +``` + +可以看到性能差距非常大,仅仅一个字符会导致std::string性能差别这么大! + +这里本质还是前面的SSO local buffer。 + +性能测试:https://quick-bench.com/q/s7pvDB_SwQyDCcZk50odAaYIzMc + +![](../assets/images/2024/bench/img/gcc_short_long.png) + +## 2.clang++(libc++) + +同理,我们以llvm-14为例,如果想要看完整代码戳下面的github地址即可,这里摘取出核心代码。 + +> https://github.com/llvm/llvm-project/blob/release/14.x/libcxx/include/string#L765 + +```cpp +template +class basic_string { + private: + struct __long { + size_type __cap_; // 8 bytes + size_type __size_; // 8 bytes + pointer __data_; // 8 bytes + }; + + enum {__min_cap = (sizeof(__long) - 1)/sizeof(value_type) > 2 ? + (sizeof(__long) - 1)/sizeof(value_type) : 2}; + + struct __short { + union { + unsigned char __size_; // 1 bytes + value_type __lx; // 1 bytes + }; // 1 bytes + value_type __data_[__min_cap]; // 23 bytes + }; // 24 bytes + + union __ulx { + __long __lx; // 24 bytes + __short __lxx; // 24 bytes + }; // 24 bytes +} +``` + +在上面代码中注释部分详细的列出了string占据的字节(注意:这里的value_type以char来计算,也就是sizeof(value_type) = 1)。 + +sizeof(string) = union的大小,这个union是24字节,所以等于24。 + +而SSO,本地buffer存储的字符不超过22,当超过22个字符便会分配在heap上。 + +所以当我们使用与gcc同样的测试代码应用在clang上,我们便得到了不太一样的结果,即:long字符串的性能与short字符串的性能拆别不大。原因就是gcc的测试case都是在22个字符以内,都是短字符。 + +性能测试:https://quick-bench.com/q/nmLMqQZ31cmWBbbIFr2nHN2JL1k + +![](../assets/images/2024/bench/img/llvm_short_long_no.png) + +当我们测试真正的长短字符串时,性能便会发生明显变化,趋势与gcc保持一致。 + +- 短字符串测试内容为:"hello world hello worl",总共22个字符。 +- 长字符串测试内容为:"hello world hello world",总共为23个字符。 + +性能测试:https://quick-bench.com/q/ujtMrtotEi6RrpfMDJ1UcSLFIJE + +![](../assets/images/2024/bench/img/llvm_short_long.png) \ No newline at end of file diff --git a/site_extras/cpp-benchmarks-memcpy-vs-std-copy.md b/site_extras/cpp-benchmarks-memcpy-vs-std-copy.md new file mode 100644 index 0000000..f1d1260 --- /dev/null +++ b/site_extras/cpp-benchmarks-memcpy-vs-std-copy.md @@ -0,0 +1,27 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-benchmarks/memcpy-vs-std-copy/) 的文章,由站点同步脚本导出。 + +# memcpy 与 std::copy 性能对比 + +最近在做性能优化时,发现有一些代码用了std::copy,还有一些用了memcpy,两者在功能上其实类似,那么它们对性能影响是怎样的呢? + +于是本篇文章将会探讨这两者的一些使用场景。 + +## memcpy:理论上有一些性能优势 + +从理论上讲,`memcpy`可能具有轻微的性能优势,原因在于其不同于`std::copy`的要求。`memcpy`在处理数据拷贝时可以忽略数据重叠的可能性,而`std::copy`需要处理一些不同的情况(底层memmove)。然而,这种性能差异在实际应用中往往难以察觉,特别是对于小规模的数据拷贝操作。 + +值得注意的是,`memcpy`只能用于处理C风格的POD结构,因此在处理非POD类型时并不是一个可行的选择。 + +这里使用quick-bench做了一个测评,可以看到性能并无太大差别。 + +> https://quick-bench.com/q/QER6YyI5f33kmqNqV7_Zdw8P5AE + +![](../assets/images/2024/bench/img/copy_memcpy.png) + +## 如何选择? + +在实际项目中,如何选择`memcpy`还是`std::copy`取决于具体的需求。如果处理的是POD类型,而且希望保持灵活性,那么选择`std::copy`是更为明智的选择。对于大多数应用场景,`std::copy`提供了更通用、更安全的数据拷贝方式。 + +然而,在追求极致性能的情况下,可以考虑使用`memcpy`,尤其是在处理大规模数据时。但在做出选择时,务必结合实际应用场景,并借助性能分析工具进行详细测试和优化。 + +总的来说,`memcpy`和`std::copy`都有各自的优势和劣势,我们需要根据项目的具体要求,在性能和灵活性之间做出权衡。在实践中,通常选择更安全和通用的`std::copy`是一个更可靠的选择。 \ No newline at end of file diff --git a/site_extras/cpp-benchmarks-polymorphism-crtp-variant-virtual.md b/site_extras/cpp-benchmarks-polymorphism-crtp-variant-virtual.md new file mode 100644 index 0000000..10bb95c --- /dev/null +++ b/site_extras/cpp-benchmarks-polymorphism-crtp-variant-virtual.md @@ -0,0 +1,121 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-benchmarks/polymorphism-crtp-variant-virtual/) 的文章,由站点同步脚本导出。 + +# C++ 多态:虚函数、CRTP 与 variant + +## C++多态性能测试:CRTP vs std::variant vs virtual + +多态是面向对象编程的一个重要概念,它使得单一接口能够代表不同的类型。C++提供了几种实现多态性的方式,本文将会讨论三种场景的多态: + +1. **虚函数**:在C++中实现多态性的传统方式是使用虚函数。这涉及使用基类和派生类来实现特定的实现。 +2. **std::variant**:在C++17中引入的std::variant,它实现了一种无需继承的多态性。 +3. **CRTP(Curiously Recurring Template Pattern)**:CRTP是一种比较特殊的技术,它通过模板的奇特递归模式实现多态性。 + +测试的组合场景如下: + +- 单纯crtp +- crtp + std::variant +- virtual +- std::variant + std::visit +- std::variant + std::get_if +- std::variant + std::holds_alternative + +使用的编译器: + +- gcc 13.2 +- clang17.0 + +完整测试代码已放置**星球**,这里贴一下关键代码 + +```cpp +class Shape { +public: + virtual ~Shape() = default; + virtual double area() const = 0; +}; + +class Circle : public Shape +class Rectangle : public Shape + + + +// virtual +static void BM_VirtualFunction(benchmark::State& state) { + std::vector> shapes; + shapes.reserve(1000); + + for (int i = 0; i < 1000; ++i) { + if (i % 2 == 0) { + shapes.emplace_back(std::make_unique(5.0)); + } else { + shapes.emplace_back(std::make_unique(4.0, 6.0)); + } + } + + for (auto _ : state) { + double totalArea = 0.0; + for (const auto& shape : shapes) { + totalArea += shape->area(); + } + benchmark::DoNotOptimize(totalArea); + } +} +BENCHMARK(BM_VirtualFunction); + +static void BM_VariantVisit(benchmark::State& state) { + std::vector> shapes; + shapes.reserve(1000); + + for (int i = 0; i < 1000; ++i) { + if (i % 2 == 0) { + shapes.emplace_back(Circle(5.0)); + } else { + shapes.emplace_back(Rectangle(4.0, 6.0)); + } + } + + for (auto _ : state) { + double totalArea = 0.0; + for (const auto& s : shapes) { + totalArea += std::visit([](const auto& shape) { return shape.area(); }, s); + } + benchmark::DoNotOptimize(totalArea); + } +} +static void BM_CRTPVariant(benchmark::State& state) { + std::vector shapes; + shapes.reserve(1000); + + for (int i = 0; i < 1000; ++i) { + if (i % 2 == 0) { + shapes.emplace_back(crtp_light::Circle(5.0)); + } else { + shapes.emplace_back(crtp_light::Rectangle(4.0, 6.0)); + } + } + + for (auto _ : state) { + double totalArea = 0.0; + for (const auto& s : shapes) { + totalArea += std::visit([](const auto& shape) { return shape.area(); }, s); + } + benchmark::DoNotOptimize(totalArea); + } +} +BENCHMARK(BM_CRTPVariant); + +// ... +``` + +测试结果1:gcc编译,可以看到virtual与std::variant性能差别不大,但是与crtp差别非常大。 + +![](../assets/images/2024/bench/virtual/img/gcc_bm.png) + +测试结果2:clang编译,总体趋势类似gcc编译,只有crtp + std::variant性能明显回退,这个可能也是由于这里用了std::visit导致。 + +![](../assets/images/2024/bench/virtual/img/clang_bm.png) + +在A Tour of C++书中提到: + +> This is basically equivalent to a virtual function call, but potentially faster. As with all claims of performance, this ‘‘potentially faster’’ should be verified by measurements when performance is critical. For most uses, the difference in performance is insignificant. + +与本次测试基本符合。 \ No newline at end of file diff --git a/site_extras/cpp-concurrency-cpp-deadlock-debugging.md b/site_extras/cpp-concurrency-cpp-deadlock-debugging.md new file mode 100644 index 0000000..f435d85 --- /dev/null +++ b/site_extras/cpp-concurrency-cpp-deadlock-debugging.md @@ -0,0 +1,97 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-concurrency/cpp-deadlock-debugging/) 的文章,由站点同步脚本导出。 + +# C++ 死锁排查 + +## C++如何排查并发编程死锁问题? + +最近在Apache arrow里面写一个支持并行的算子:nested loop join,然后既然涉及到并行,这里就会遇到大家常说的死锁问题,假设你碰到了死锁问题,如何调试与定位呢? + +那这便是本篇目标,帮助你快速掌握并发编程:死锁检测与死锁调试问题,非常的干,好了进行正文。 + +## 1.引入 + +为了更好的讲解死锁,我们用一个程序来引入。 + +```cpp +std::mutex gMutex; + +int t2() { + std::lock_guard m(gMutex); + return 0; +} + +int t1() { + std::lock_guard l(gMutex); + return t2(); +} +``` + +相信看这个程序,大家都会觉得有问题,死锁了!问题出在`t1()`函数和`t2()`函数中都对全局的互斥锁`gMutex`进行了加锁操作,但是`t1()`函数在加锁后调用了`t2()`函数,而`t2()`函数内部又试图再次对`gMutex`进行加锁。 + +t1锁已经加上了,但还没释放,t2又去加锁,两个人都在等待谁先释放,进入了死循环,实际在项目中代码并不会如这里这么简单,非常的复杂,例如:我在Apache arrow中写的代码是这样: + +```cpp +Status OnBuildSideFinished(size_t thread_index) { + std::lock_guard guard(probe_side_mutex_); + // do something + accumulate_build_ready_ = true; + return scheduler_->StartTaskGroup(thread_index,task_group_probe_,queued_batches_to_probe_.batch_count()); +} +``` + +你看这个代码比上面的场景就复杂多了,嵌套了至少5层堆栈,剩余代码还没贴出来,但是这两个本质都是一个死锁模型。 + +## 2.调试 + +讲解了死锁模型之后,碰到这种问题,如何定位呢? + +这里可以采用两种办法,第一种直接运行程序,然后gdb上去。 + +例如: + +``` +./a.out +``` + +然后找到进程号后: + +``` +gdb -p xxx +``` + +此时我们可以得到及格正在等待的线程。 + +```cpp +(gdb) info threads + Id Target Id Frame +* 1 Thread 0x7ffff7fe2740 (LWP 32301) "a.out" 0x00007ffff7bc8017 in pthread_join () from /lib64/libpthread.so.0 + 2 Thread 0x7ffff6fd0700 (LWP 32305) "a.out" 0x00007ffff7bcd54d in __lll_lock_wait () from /lib64/libpthread.so.0 +``` + +然后去看`__lll_lock_wait`的堆栈,例如这里我看了2号线程,然后查看堆栈得到t1与t2的行号,直接可以定位到哪里出了问题,非常的直观! + + + +除了这种方式之外,还可以直接gdb上去运行程序,此时会卡死,然后ctrl + c杀掉之后也是可以跟上面一样的内容。 + +例如: + +``` +(gdb) r +Starting program: /home/light/a.out +Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64 +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/lib64/libthread_db.so.1". +Hello World! +[New Thread 0x7ffff6fd0700 (LWP 32305)] +^C +Thread 1 "a.out" received signal SIGINT, Interrupt. +0x00007ffff7bc8017 in pthread_join () from /lib64/libpthread.so.0 +Missing separate debuginfos, use: debuginfo-install libgcc-4.8.5-44.el7.x86_64 libstdc++-4.8.5-44.el7.x86_64 +(gdb) info threads + Id Target Id Frame +* 1 Thread 0x7ffff7fe2740 (LWP 32301) "a.out" 0x00007ffff7bc8017 in pthread_join () from /lib64/libpthread.so.0 + 2 Thread 0x7ffff6fd0700 (LWP 32305) "a.out" 0x00007ffff7bcd54d in __lll_lock_wait () from /lib64/libpthread.so.0 +``` + +好了,本节就讲这么多,感兴趣的欢迎转发这篇硬核文章!于此同时,推荐一下自己的**线程池项目**,个人用视频讲解了这个项目,同时配上docker一键部署(这一期的视频,后面打算给大家录制一下),感兴趣的联系我即可。 \ No newline at end of file diff --git a/site_extras/cpp-concurrency-cpp-simd.md b/site_extras/cpp-concurrency-cpp-simd.md new file mode 100644 index 0000000..1c07c4f --- /dev/null +++ b/site_extras/cpp-concurrency-cpp-simd.md @@ -0,0 +1,174 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-concurrency/cpp-simd/) 的文章,由站点同步脚本导出。 + +# C++ SIMD 向量化 + +最近在看相关向量化的内容,看起来有点头大,借此机会,学习一下高性能SIMD编程。 + +SIMD全称single-instruction multiple-data,单指令多数据。 + +在传统的计算机架构中,CPU一次只能处理一个数据元素。但是,许多任务涉及对大量数据执行相同的操作,例如对数组中的所有元素进行加法、乘法或逻辑操作等。SIMD编程通过向CPU提供专门的指令集,使得CPU能够同时对多个数据元素执行相同的操作。 + +这种处理方式特别适合涉及向量、矩阵、图像、音频和视频等数据的计算。 + +目前比较常用的有SSE、SSE2、AVX128、AVX256、AVX512。 + +本节,将简单学习一下AVX512的一些操作,操作比较多,这里只是引入一些。 + +## 1.术语 + +首先第一个问题便是,simd编程的代码跟平时写的代码长相不大一样,各种下划线以及命名,完全看不懂,如何理解呢? + +诸如: + +- _mm512_set1_ps + +Broadcast single-precision (32-bit) floating-point value a to all elements of dst. + +- _mm512_set1_epi32 + +Broadcast 32-bit integer a to all elements of dst. + +于是,找到了下面这个表格: + +| Abbreviation | Full Name | C/C++ Equivalent | +| ------------ | ------------------------------ | ---------------- | +| ps | packed single-precision | float | +| ph | packed half-precision | None* | +| pd | packed double-precision | double | +| pch | packed half-precision complex | None* | +| pi8 | packed 8-bit integer | int8_t | +| pi16 | packed 16-bit integer | int16_t | +| pi32 | packed 32-bit integer | int32_t | +| epi8 | extended packed 8-bit integer | int8_t | +| epi16 | extended packed 16-bit integer | int16_t | +| epi32 | extended packed 32-bit integer | int32_t | +| epi64 | extended packed 64-bit integer | int64_t | +| epi64x | extended packed 64-bit integer | int64_t | + +> https://stackoverflow.com/questions/70911872/what-are-the-names-and-meanings-of-the-intrinsic-vector-element-types-like-epi6 + +再比如: + +_mm512_mask_load_ps + +_mm512_mask_loadu_ps + +u表示unordered,表示加载无序,当使用 `_mm512_mask_loadu_ps` 函数加载内存中的数据时,不会执行对内存地址的任何对齐要求。而_mm512_mask_load_ps要求满足 64 字节对齐要求。 + +这样对照着学习,非常快的便可以知道每个接口的含义了。 + +相关API可以看看Interl Intrinsics Guide。 + +> https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html + +## 2.实际例子 + +### 2.1 求最小值 + +对于512位,我们可以存储16个32位float。 + +```cpp +static inline rf_512 load(float* ptr) { return _mm512_loadu_ps(ptr); } +float a[width] = {1.0, 2.0, 3.0, 22.0, 5.0, 17.0, 9.0, 8.0, + 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0}; +float b[width] = {3.3, 6.2, 5.3, 4.4, 5.5, 6.6, 7.7, 8.8, + 9.9, 10.10, 21.11, 12.12, 13.13, 14.14, 15.0, 16.16}; +rf_512 data = minimum(load(a), load(b)); +``` + +于是我们可以快速得到: + +```cpp +1 2 3 4.4 5 6.6 7.7 8 9 10 11 12 13 14 15 16 +``` + +## 2.2 快速打乱数据顺序 + +对于输入数据是 + +``` +1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 +``` + +我们可以快速得到: + +```cpp +16.16 2 14.14 13.13 5 6 7 8 9 10 11 12 13 14 15 16 +``` + +对应实现: + +```cpp +static inline rf_512 permutexvar(ri_512 idx, rf_512 src) { + return _mm512_permutexvar_ps(idx, src); +} +/* +raw data: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 +shuffle data: 16.16 2 14.14 13.13 5 6 7 8 9 10 11 12 13 14 15 16 +*/ +void print_permutexvar_mask() { + float a[width] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0}; + float b[width] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, + 9.9, 10.10, 11.11, 12.12, 13.13, 14.14, 15.0, 16.16}; + mask_type mask = make_bit_mask<1, 0, 1, 1, 0>(); + int idx_array[width] = {15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; + ri_512 idx = _mm512_loadu_si512((__m512i*)idx_array); + rf_512 result = permutexvar_mask(load(a), mask, idx, load(b)); + float result_arr[width]; + store(result, result_arr); + std::cout << "raw data: "; + for (int i = 0; i < width; i++) { + std::cout << a[i] << " "; + } + std::cout << std::endl; + + std::cout << "shuffle data: "; + for (int i = 0; i < width; i++) { + std::cout << result_arr[i] << " "; + } + std::cout << std::endl; +} +``` + +### 2.3 旋转 + +对于一个数组: + +``` +1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 +``` + +我们可以旋转得到: + +``` +4 5 6 7 8 9 10 11 12 13 14 15 16 1 2 3 +``` + +or + +``` +14 15 16 1 2 3 4 5 6 7 8 9 10 11 12 13 +``` + +也就是rotate down or rotate up。 + +在这里,我们可以这样实现: + +```cpp +rf_512 rotateGeneral(float* arr, int s) { + int idx_array[width]; + for (int i = 0; i < width; i++) { + idx_array[i] = (i + s) % width; + } + ri_512 idx = _mm512_loadu_si512((__m512i*)idx_array); + rf_512 result = permutexvar(idx, load(arr)); + return result; +} +``` + +等等,还有其他的例子,可以发现通过使用simd,我们可以实现一些非常有趣的算法,加速对数组,批量数据的处理。 + +后面会继续学习simd,一起加油吧~ + +注:本节的完整示例已在星球更新,谢谢~ \ No newline at end of file diff --git a/site_extras/cpp-concurrency-cpp17-parallel-stl.md b/site_extras/cpp-concurrency-cpp17-parallel-stl.md new file mode 100644 index 0000000..e719050 --- /dev/null +++ b/site_extras/cpp-concurrency-cpp17-parallel-stl.md @@ -0,0 +1,145 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-concurrency/cpp17-parallel-stl/) 的文章,由站点同步脚本导出。 + +# C++17 并行 STL + +## 1.各个编译器支持进度 + +不同编译器对并行算法库的支持,如下: + +> https://en.cppreference.com/w/cpp/compiler_support/17#C.2B.2B17_library_features + +可以看到gcc9之后就支持了,clang目前支持还不完全,自己也本地测了一下确实不支持。 + +![](../assets/images/2024/parallel/img/fea.png) + +其支持的算法包括: + +![](../assets/images/2024/parallel/img/all_std.png) + +> https://en.cppreference.com/w/cpp/experimental/parallelism + +## 2.并行算法库要求 + +gcc9以后便可以使用并行算法库了,我们可以看一下下面的gcc-9文档。 + +> https://gcc.gnu.org/gcc-9/changes.html + +其中,提到一个非常重要的信息:**需要tbb支持**。 + +![](../assets/images/2024/parallel/img/tbb_req.png) + +TBB是由英特尔(Intel)公司开发的一个用于并行编程的 C++ 库。TBB 旨在简化多核处理器上的并行软件开发,提供了一套抽象和模型,使程序员能够更容易地实现并行算法。 + +1.C++17当中使用并行算法库的前提必须得安装tbb,那如果不安装tbb会怎样呢? + +我自己实测就是可以编译与运行,只是没有任何加速,跟原来非并行跑出来的结果是一样的。 + +2.TBB如何安装呢? + +一个是官方的tbb,叫做oneTBB。 + +> https://github.com/oneapi-src/oneTBB + +另一个是其他人写的tbb,叫做tbb。 + +> https://github.com/wjakob/tbb + +区别在于添加了基于 CMake 的构建系统。 + +现在官方也是支持cmake ,所以建议去使用官方的吧。 + +编译也是非常简单: + +``` +mkdir build && cmake .. +make -j8 +make install +``` + +## 3.如何使用 + +相比于传统的STL使用又有哪些改变呢? + +**1.接口层面** + +其实比较简单,例如 sort原来是: + +```cpp +std::sort(begin, end, comp); +``` + +现在变为: + +``` +std::sort(exe_policy, begin, end, comp); +``` + +只需要添加执行策略(execution policy),有三个选项: + +1. **`std::execution::seq`(顺序执行):** + - 这是默认的执行策略,也就是说,如果你没有明确指定执行策略,算法将使用顺序执行。 + - 算法将按照顺序依次处理元素,不涉及并行处理。 +2. **`std::execution::par`(并行执行):** + - 使用这个执行策略将启用并行执行。算法会尝试以并行的方式处理元素,充分利用多核处理器。 +3. **`std::execution::par_unseq`(并行和向量化执行):** + - 这个执行策略允许算法在并行的同时进行向量化(vectorization),这意味着可以利用 SIMD(Single Instruction, Multiple Data)指令集进行并行向量化操作。 + - 与 `std::execution::par` 不同,`par_unseq` 在能够并行处理的同时还允许矢量化处理,从而更充分地利用硬件的并行和向量化能力。 + +**2.头文件层面** + +必须要引入: + +```cpp +#include +#include +``` + +如果有tbb的操作,可以引入tbb的头文件,例如:设置线程数。 + +**3.编译层面** + +编译时需要指定链接信息,例如: + +```cpp +g++ -O3 -std=c++17 -Wall -Wextra -pedantic -I/usr/local/include/tbb -L /usr/local/lib/ -o main.out main.cc -ltbb +``` + +## 4.性能测试 + +最近在做sort相关的工作,想看看merge与sort的性能,所以写了个benchmark,逻辑如下: + +- 串行sort vs 并行sort(par策略) + +- 串行merge vs 并行merge(par策略) + +数据范围: + +```cpp +BENCHMARK(BM_ParallelSort)->Arg(16384)->Arg(16384000)->Arg(163840000); +``` + +下面的测试结果将采用tbb默认线程调度策略。 + +**对比组1:并行Sort/串行Sort详细数据与加速比** + +![](../assets/images/2024/parallel/img/sort_parallel.png) + +![](../assets/images/2024/parallel/img/sort_parallel_sp.png) + +**对比组2:并行Merge/串行Merge详细数据与加速比** + +![](../assets/images/2024/parallel/img/merge_parallel.png) + +![](../assets/images/2024/parallel/img/merge_parallel_sp.png) + +tbb文档: + +> https://oneapi-src.github.io/oneTBB/index.html + +如果要设置线程数,可以这样操作: + +```cpp +#include + +tbb::task_scheduler_init init(16); +``` \ No newline at end of file diff --git a/site_extras/cpp-concurrency-false-sharing-cache-line.md b/site_extras/cpp-concurrency-false-sharing-cache-line.md new file mode 100644 index 0000000..4f0e1b1 --- /dev/null +++ b/site_extras/cpp-concurrency-false-sharing-cache-line.md @@ -0,0 +1,54 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-concurrency/false-sharing-cache-line/) 的文章,由站点同步脚本导出。 + +# C++ False Sharing 与缓存行 + +最近看到一段代码,手动做的对齐,于是研究一下不对齐又会带来什么影响? + +```cpp +template +class AtomicWithPadding { + private: + static constexpr int kCacheLineSize = 64; + uint8_t padding_before[kCacheLineSize]; + + public: + std::atomic value; + + private: + uint8_t padding_after[kCacheLineSize]; +}; +``` + +这里便引出了大家面试的必考点: + +- Cache line + +于是有下面几个面试问题,你能答上来? + +- 什么是cache line? +- 什么是MESI协议? +- 什么是False sharing? +- C++里面如何保证避免False sharing? +- 避免False sharing可以带来什么好处? +- 你知道C++17之后提供了哪两个变量解决这个问题? + +于是,找了个性能测试,大概实现是: + +- 定义4个变量,a、b、c、d,多线程进行共享 +- 另外是对这四个变量对齐,然后开n个线程去调度。 + +下面是benchmark: + +```cpp +------------------------------------------------------------------ +Benchmark Time CPU Iterations +------------------------------------------------------------------ +singleThread 4.05 ms 4.04 ms 133 +directSharing/real_time 8.78 ms 0.069 ms 77 +falseSharing/real_time 8.81 ms 0.083 ms 74 +noSharing/real_time 1.16 ms 0.071 ms 578 +``` + +可以看到noSharing得到了非常好的效果!缓存命中率大大提高,当然这里用perf分析一下是更好的,可以看看L1 cache miss。 + +以上答案在知识星球已更新,欢迎一起探讨,留言区交流,谢谢~ \ No newline at end of file diff --git a/site_extras/cpp-concurrency-gdb-multi-thread-debug.md b/site_extras/cpp-concurrency-gdb-multi-thread-debug.md new file mode 100644 index 0000000..cd3fcc6 --- /dev/null +++ b/site_extras/cpp-concurrency-gdb-multi-thread-debug.md @@ -0,0 +1,137 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-concurrency/gdb-multi-thread-debug/) 的文章,由站点同步脚本导出。 + +# GDB 多线程 All-Stop 调试 + +当我们使用gdb调试多线程程序时,线程可以在任何时间执行,当我们想尝试调试某一个线程的时候(例如:next),会导致切换到其他线程,不符合我们在当前线程切到下一行的预期。 + +```cpp +Thread 2 "arrow-compute-n" hit Breakpoint 1, arrow::compute::NestedLoopJoinNode::InputReceived (this=0x5bbf90, input=0x5ba840, batch=...) + at /code/arrow/cpp/src/arrow/compute/exec/nested_loop_node.cc:399 +399 size_t thread_index = thread_indexer_(); +(gdb) n +[Switching to Thread 0x7ffff2c08700 (LWP 29121)] + +Thread 3 "arrow-compute-n" hit Breakpoint 1, arrow::compute::NestedLoopJoinNode::InputReceived (this=0x5bbf90, input=0x5bb060, batch=...) + at /code/arrow/cpp/src/arrow/compute/exec/nested_loop_node.cc:399 +399 size_t thread_index = thread_indexer_(); +``` + +此时我们需要怎么做呢? + +那便是今天要引入的调度器锁定(scheduler-locking)。GDB 无法锁步执行所有线程。由于线程调度取决于你调试目标的操作系统(而不是由 GDB 控制),因此在当前线程完成单步操作时,其他线程可能执行多个语句。此外,一般来说,当程序停止时,其他线程停在语句的中间,而不是在清晰的语句边界处停止。甚至在继续或单步执行后,你可能会发现你的程序停在另一个线程中。每当其他线程在第一个线程完成你请求的操作之前遇到断点、信号或异常时,就会发生这种情况。 + +而通过gdb提供的scheduler-locking可以帮助你锁定调度器以仅允许单个线程运行来修改 GDB 的默认行为。 + +默认情况下scheduler-locking值等于replay + +``` +(gdb) show scheduler-locking +Mode for locking scheduler during execution is "replay". +``` + +在回放模式下的行为类似于on,在记录模式下或正常执行期间的行为类似于关闭。这是默认模式。 + +- on + +例如执行`t 2`会切换到线程2上面,此时设置on会只在当前线程进行调试。 + +- off + +与on相反,没有锁定,任何线程随时都可以运行。 + +- step + +在单步执行时的行为类似于打开,否则类似于关闭。当你step时,除了当前线程之外的线程永远不会有机会运行,而当你使用像`continue`、`until`或`finish`这样的命令时,它们完全可以运行。 + +例如:next、step这种会在你想要的进程内进行调试,其他线程hang住;contine之类的会让所有的线程运行。 + +## 示例 + +可以看到当设置step时,continue会在线程之间切换。 + +``` +(gdb) info threads + Id Target Id Frame + 1 Thread 0x7ffff7e88780 (LWP 31464) "arrow-compute-n" 0x00007ffff37e3de2 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 + 2 Thread 0x7ffff3409700 (LWP 31494) "arrow-compute-n" arrow::compute::NestedLoopJoinNode::InputReceived (this=0x5bbf90, input=0x5bb060, batch=...) + at /code/arrow/cpp/src/arrow/compute/exec/nested_loop_node.cc:399 +* 3 Thread 0x7ffff2c08700 (LWP 31495) "arrow-compute-n" arrow::compute::NestedLoopJoinNode::InputReceived (this=0x5bbf90, input=0x5bb060, batch=...) + at /code/arrow/cpp/src/arrow/compute/exec/nested_loop_node.cc:399 +(gdb) c +Continuing. +[Switching to Thread 0x7ffff3409700 (LWP 31494)] + +Thread 2 "arrow-compute-n" hit Breakpoint 1, arrow::compute::NestedLoopJoinNode::InputReceived (this=0x5bbf90, input=0x5bb060, batch=...) + at /code/arrow/cpp/src/arrow/compute/exec/nested_loop_node.cc:399 +399 size_t thread_index = thread_indexer_(); +(gdb) info threads + Id Target Id Frame + 1 Thread 0x7ffff7e88780 (LWP 31464) "arrow-compute-n" 0x00007ffff37e3de2 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 +* 2 Thread 0x7ffff3409700 (LWP 31494) "arrow-compute-n" arrow::compute::NestedLoopJoinNode::InputReceived (this=0x5bbf90, input=0x5bb060, batch=...) + at /code/arrow/cpp/src/arrow/compute/exec/nested_loop_node.cc:399 + 3 Thread 0x7ffff2c08700 (LWP 31495) "arrow-compute-n" 0x00007ffff605a372 in arrow::compute::NestedLoopJoinNode::InputReceived (this=0x5bbf90, input=0x5bb060, batch=...) + at /code/arrow/cpp/src/arrow/compute/exec/nested_loop_node.cc:399 +``` + +然后再continue会进入到下一个断点: + +```cpp +(gdb) info threads + Id Target Id Frame + 1 Thread 0x7ffff7e88780 (LWP 31464) "arrow-compute-n" 0x00007ffff37e3a35 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 + 2 Thread 0x7ffff3409700 (LWP 31494) "arrow-compute-n" 0x00007ffff5781b50 in arrow::compute::NestedLoopJoinNode::OnBuildSideBatch(unsigned long, arrow::compute::ExecBatch)@plt () + from /code/arrow/cpp/build/debug/libarrow.so.200 +* 3 Thread 0x7ffff2c08700 (LWP 31495) "arrow-compute-n" 0x00007ffff5781b50 in arrow::compute::NestedLoopJoinNode::OnBuildSideBatch(unsigned long, arrow::compute::ExecBatch)@plt () + from /code/arrow/cpp/build/debug/libarrow.so.200 +(gdb) c +Continuing. +[Switching to Thread 0x7ffff3409700 (LWP 31494)] + +Thread 2 "arrow-compute-n" hit Breakpoint 3, 0x00007ffff5781b50 in arrow::compute::NestedLoopJoinNode::OnBuildSideBatch(unsigned long, arrow::compute::ExecBatch)@plt () + from /code/arrow/cpp/build/debug/libarrow.so.200 +(gdb) info threads + Id Target Id Frame + 1 Thread 0x7ffff7e88780 (LWP 31464) "arrow-compute-n" 0x00007ffff37e3a35 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 +* 2 Thread 0x7ffff3409700 (LWP 31494) "arrow-compute-n" 0x00007ffff5781b50 in arrow::compute::NestedLoopJoinNode::OnBuildSideBatch(unsigned long, arrow::compute::ExecBatch)@plt () + from /code/arrow/cpp/build/debug/libarrow.so.200 + 3 Thread 0x7ffff2c08700 (LWP 31495) "arrow-compute-n" 0x00007ffff5781b56 in arrow::compute::NestedLoopJoinNode::OnBuildSideBatch(unsigned long, arrow::compute::ExecBatch)@plt () + from /code/arrow/cpp/build/debug/libarrow.so.200 +``` + +可以看到线程之间切换,如果是想在线程内,可以直接next,例如: + +```cpp +(gdb) t 2 +[Switching to thread 2 (Thread 0x7ffff3409700 (LWP 32396))] +#0 0x00007ffff605ae0d in arrow::compute::NestedLoopJoinNode::OnBuildSideBatch (this=0x5bbf90, thread_index=1, batch=...) at /code/arrow/cpp/src/arrow/compute/exec/nested_loop_node.cc:487 +487 if (!batch.length) { +(gdb) n +490 std::lock_guard guard(build_side_mutex_); +(gdb) n +491 build_accumulator_.InsertBatch(std::move(batch)); +``` + +如果是执行print操作,引起了函数调用,会停止,输出不了我们符合预期的结果。 + +```cpp +(gdb) p batch.ToString() +[Switching to Thread 0x7ffff2c08700 (LWP 32397)] + +Thread 3 "arrow-compute-n" hit Breakpoint 1, arrow::compute::NestedLoopJoinNode::InputReceived (this=0x5bbf90, input=0x5ba840, batch=...) + at /code/arrow/cpp/src/arrow/compute/exec/nested_loop_node.cc:399 +399 size_t thread_index = thread_indexer_(); +The program stopped in another thread while making a function call from GDB. +Evaluation of the expression containing the function +(arrow::compute::ExecBatch::ToString() const) will be abandoned. +When the function is done executing, GDB will silently stop. +``` + +我们此时可以设置on,此时便可以进行print了! + +``` +(gdb) set scheduler-locking on +(gdb) p batch.ToString() +$2 = "ExecBatch\n # Rows: 2\n 0: Array[null,4]\n 1: Array[true,false]\n" +``` + +> https://sourceware.org/gdb/current/onlinedocs/gdb.html/All_002dStop-Mode.html \ No newline at end of file diff --git a/site_extras/cpp-concurrency-helgrind-concurrency-check.md b/site_extras/cpp-concurrency-helgrind-concurrency-check.md new file mode 100644 index 0000000..6b64713 --- /dev/null +++ b/site_extras/cpp-concurrency-helgrind-concurrency-check.md @@ -0,0 +1,77 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-concurrency/helgrind-concurrency-check/) 的文章,由站点同步脚本导出。 + +# Helgrind 并发错误检测 + +## C++那些事之helgrind并发编程检测 + +通常我们在写多线程程序的时候很容易遇到两个问题: + +- 死锁了,不知道什么原因导致 +- 数据不一致,多个线程锁用错了 + +那么有没有工具来检测这两种场景呢 + +答案是有的,我们可以使用valgrind的helgrind工具检测这两个问题,为了使本文讲解的更加丝滑,引出了几个例子。 + +**注:完整示例及修复示例已更新至星球。** + +## 死锁 + +假设有两个线程互相持有对方的锁,此时我们可以模拟出死锁,例如: + +```cpp +void thread_func1() { + std::lock_guard lock1(mutex1); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::lock_guard lock2(mutex2); +} + +void thread_func2() { + std::lock_guard lock2(mutex2); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::lock_guard lock1(mutex1); +} +``` + +helgrind使用的方法如下: + +``` +valgrind --tool=helgrind ./a.out +``` + +此时我们可以看到死锁的位置,例如这个示例中指出了两个线程在对应位置发生了死锁 + +``` +thread_func2() (deadlock.cc:18) +thread_func1() (deadlock.cc:12) +``` + +![](../assets/images/2024/img/deadlock.png) + +## 数据竞争 + +假设有一个共享变量,我们在多线程对它增加10w次,这个结果是多少呢? + +很明显这里产生了多线程竞争全局变量的问题,最终的结果不一致,使用helgrind检测方法同上。 + +```cpp +int shared_var = 0; + +void increment() { + for (int i = 0; i < 100000; ++i) { + ++shared_var; + } +} +``` + +此时,我们可以看到data race的行数是哪一行,然后去解决问题。 + +![](../assets/images/2024/img/datarace.png) + +最后,留几个问题吧: + +1.上面这两个有问题的代码,如何修复? + +2.除了上面工具,还有其他办法? + +> 本节完 \ No newline at end of file diff --git a/site_extras/cpp-concurrency-libev-event-loop.md b/site_extras/cpp-concurrency-libev-event-loop.md new file mode 100644 index 0000000..641e765 --- /dev/null +++ b/site_extras/cpp-concurrency-libev-event-loop.md @@ -0,0 +1,265 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-concurrency/libev-event-loop/) 的文章,由站点同步脚本导出。 + +# libev 事件循环原理 + +大纲 + +[TOC] + +`libev` 是一个跨平台的高效事件通知库,主要用于多路复用事件机制(例如 `select`、`poll`、`epoll` 等)的实现。它的设计目标是提供简单、快速的事件循环机制,特别适合需要高并发和低延迟的场景,如高性能 Web 服务器、网络代理和其他 IO 密集型应用。它的代码量非常少,并且使用在一些生产环境中,非常推荐阅读,本节来讲讲其中的实现原理与使用。 + +## 1.基本框架 + +libev 的核心是事件循环(event loop),它是应用程序与操作系统之间的接口,用于监听和分发不同类型的事件。libev 的架构非常简洁,主要包含以下几个部分: + +### 1.1 事件句柄(ev_watcher) + +libev 通过事件句柄(watcher)来管理和监听各种事件。每种事件类型(如 IO 事件、定时器事件等)都有一个对应的 watcher 类型。常见的事件句柄包括: + +- **ev_io**:用于监听文件描述符的可读/可写事件。 +- **ev_timer**:用于定时器事件,支持一次性定时器和重复定时器。 +- **ev_signal**:用于捕捉信号事件(如 `SIGINT`、`SIGTERM` 等)。 + +像上面都可以称之为ev_watcher的子类: + +```cpp +typedef struct ev_watcher +{ + int active; + int pending; + int priority; + void *data; + void (*cb)(struct ev_loop *loop, struct ev_watcher *w, int revents); +} ev_watcher; +``` + +实际使用时,会涉及到两个关键函数,例如:注册一个可读的标准输入事件 + +```cpp +ev_io stdin_watcher; +ev_io_init(&stdin_watcher, stdin_cb, /*STDIN_FILENO*/ 0, EV_READ); +ev_io_start(loop, &stdin_watcher); +``` + +ev_io_init是一个宏,最后展开就是下面代码,主要是对watcher进行初始化,例如:设置callback、fd、events等。 + +```cpp + do + { + do + { + ((ev_watcher *)(void *)((&stdin_watcher)))->active = ((ev_watcher *)(void *)((&stdin_watcher)))->pending = 0; + ((ev_watcher *)(void *)(((&stdin_watcher))))->priority = (0); + ((((&stdin_watcher)))->cb = ((stdin_cb)), memmove(&((ev_watcher *)(((&stdin_watcher))))->cb, &(((&stdin_watcher)))->cb, sizeof((((&stdin_watcher)))->cb))); + } while (0); + do + { + ((&stdin_watcher))->fd = ((0)); + ((&stdin_watcher))->events = ((EV_READ)) | EV__IOFDSET; + } while (0); + } while (0); +``` + +ev_io_start主要是修改anfs与fdchanges数组。 + +- 对于已经active的watcher直接返回 +- 如果还不活跃,则设置active与priority +- 使用头插法插入到anfds对应fd的链表中,并将当前watcher设置为head + +```cpp +wlist_add(&((loop)->anfds)[fd].head, (WL)w); + +static __inline__ void +wlist_add(WL *head, WL elem) +{ + elem->next = *head; + *head = elem; +} +``` + +- 将fd插入到fdchanges数组末尾 + +```cpp +++((loop)->fdchangecnt); +((loop)->fdchanges)[((loop)->fdchangecnt) - 1] = fd; +``` + +- 设置标记 + +```cpp +w->events &= ~EV__IOFDSET; +``` + +### 1.2 IO多路复用 + +在libev中支持epoll、poll、select等,其中在loop中有2个回调函数: + +```cpp +void (*backend_modify)(struct ev_loop *loop, int fd, int oev, int nev); +void (*backend_poll)(struct ev_loop *loop, ev_tstamp timeout); +``` + +我们以epool为例,backend_modify就是epoll_modify,backend_poll就是epoll_poll。 + +epoll_modify就是大家通常使用的epoll_ctl,当事件发生变化使用EPOLL_CTL_MOD,否则使用EPOLL_CTL_ADD。 + +```cpp +epoll_ctl (backend_fd, oev && oldmask != nev ? EPOLL_CTL_MOD : EPOLL_CTL_ADD, fd, &ev) +``` + +epoll_poll执行逻辑如下: + +- epoll_wait获取就绪事件列表(loop->epoll_events) +- 对于就绪事件,执行fd_event放入loop的pendings对应优先级的数组中。 + +```cpp +static __inline__ void +fd_event(struct ev_loop *loop, int fd, int revents) +{ + ANFD *anfd = ((loop)->anfds) + fd; + if (__builtin_expect((!!(!anfd->reify)), (1))) + fd_event_nocheck(loop, fd, revents); +} + +static __inline__ void +fd_event_nocheck(struct ev_loop *loop, int fd, int revents) +{ + ANFD *anfd = ((loop)->anfds) + fd; + ev_io *w; + for (w = (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next) + { + int ev = w->events & revents; + if (ev) + ev_feed_event(loop, (W)w, ev); + } +} + +// 省略部分代码 +void __attribute__((__noinline__)) +ev_feed_event(struct ev_loop *loop, void *w, int revents) +{ + w_->pending = ++((loop)->pendingcnt)[pri]; + ((loop)->pendings)[pri][w_->pending - 1].w = w_; + ((loop)->pendings)[pri][w_->pending - 1].events = revents; +} +``` + +### 1.3 Timer + +Timer内部实现采用了二叉堆与四叉堆,这里之所用四叉堆,是因为四叉堆的缓存效率更高,其实现如下: + +```cpp +#define DHEAP 4 +#define HEAP0 (DHEAP - 1) /* index of first element in heap */ +#define HPARENT(k) ((((k) - HEAP0 - 1) / DHEAP) + HEAP0) +#define UPHEAP_DONE(p,k) ((p) == (k)) +``` + +注意上面使用了HEAP0作为第一个元素,即offset,堆内部的实现便是upheap、downheap,比较简单就不赘述。其结构为: + +```cpp +typedef ev_watcher_time *WT; + +typedef struct +{ + ev_tstamp at; + WT w; +} ANHE; +``` + +heap的排序是按照at来比较,at为定时器到期时间,堆排序则按照at的值由小到大排序。 + +### 1.4 事件循环(ev_loop) + +通常使用如下: + +```cpp +struct ev_loop *loop = EV_DEFAULT; +ev_run(loop, 0); +``` + +EV_DEFAULT会调用ev_default_loop函数,初始化一个ev_loop结构,里面主要是通过loop_init初始化loop结构,比如:选择epoll还是poll、select。 + +最核心的点:epoll_init发生在上面loop创建时。 + +ev_run比较复杂,主要做几件事: + +- fd_reify + +遍历前面提到的fdchanges数组,根据fd从anfds中取出链表,遍历所有事件链表,得到所有事件,判断当前最新的事件与之前老的事件(events)是否一致,不一致调用epoll_modify(或者第一次添加事件)。 + +```cpp +typedef struct +{ + WL head; // 事件链表 + unsigned char events; // 前面事件 + // other +} ANFD; +``` + +- backend_poll + +见前面backend_poll的实现,会获取就绪事件并把事件放入pendings数组里面,即放到对应的优先级队列的最后。 + +- timer_reify,事件未过期不做处理,过期了执行下面逻辑: + + - 没有设置repeat的timer + - 调用ev_timer_stop清除 + + - 设置repeat的timer + + 例如:`ev_timer_init(&timer_watcher, timer_cb, 2.0, 3.0);`` + + ``ev_timer` 的 `at` 设置为当前时间加上 2 秒(即定时器将在 2 秒后触发),并且 `repeat` 设置为 3 秒,那么定时器将首先在 2 秒后触发,然后每隔 3 秒触发一次。 + + ```cpp + if (w->repeat) + { + // 更新下次触发时间 + ((WT)(w))->at += w->repeat; + // 过期了 + if (((WT)(w))->at < ((loop)->mn_now)) + ((WT)(w))->at = ((loop)->mn_now); // 设置当前时间 + // 设置堆顶元素为前面w->at + (((loop)->timers)[HEAP0]).at = (((loop)->timers)[HEAP0]).w->at; + // 调整堆 + downheap(((loop)->timers), ((loop)->timercnt), HEAP0); + } + ``` + +- Invoke_cb + +Invoke_cb默认是ev_invoke_pending,从对应优先队列中取出事件,调用callback函数进行事件处理。 + +```cpp +ANPENDING *p = ((loop)->pendings)[((loop)->pendingpri)] + --((loop)->pendingcnt)[((loop)->pendingpri)]; +(p->w)->cb(loop, (p->w), (p->events)); +``` + +## 2.如何使用? + +```cpp +git clone git@github.com:enki/libev.git +./configure +make && make install +``` + +项目编译: + +``` +gcc main.c -lev +``` + +## 3.课后作业 + +最后,留一个课后题目,你是否可以使用libev写一个这样的例子? + +当我们输入数据的时候,stdio事件返回ready,如果输入超时,则返回timeout,演示效果: + +```cpp +[light@i-7dwsk0ty libev]$ ./a.out +timeout +[light@i-7dwsk0ty libev]$ ./a.out +21 +stdin ready +``` \ No newline at end of file diff --git a/site_extras/cpp-concurrency-std-thread-internals.md b/site_extras/cpp-concurrency-std-thread-internals.md new file mode 100644 index 0000000..86b071d --- /dev/null +++ b/site_extras/cpp-concurrency-std-thread-internals.md @@ -0,0 +1,156 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-concurrency/std-thread-internals/) 的文章,由站点同步脚本导出。 + +# C++ std::thread 内核实现 + +## 深度揭秘std::thread + +当我们在写一个lambda时,传给thread,线程创建与task调用是在何时触发? + +```cpp +auto t = std::thread([this] { + // do something +}); +``` + +这这个线程中传递了一个lambda,那么本节所要解决的问题是:这个lambda何时触发?是在构造的时候?还是在某处调用时? + +来,让我们一起进入源码时代。 + +## 1._M_make_routine + +本节所分析的代码分别如下: + +1) 头文件 + +> https://github.com/gcc-mirror/gcc/blob/releases/gcc-4.9/libstdc%2B%2B-v3/include/std/thread + +2. 实现文件 + +> https://github.com/gcc-mirror/gcc/blob/releases/gcc-4.9/libstdc%2B%2B-v3/src/c%2B%2B11/thread.cc + +3. posix头文件 + +> https://github.com/gcc-mirror/gcc/blob/releases/gcc-4.9/libgcc/gthr-posix.h + +上面的代码会调用下面这个构造函数: + +```cpp +template +explicit +thread(_Callable&& __f, _Args&&... __args) +{ + _M_start_thread(_M_make_routine(std::__bind_simple( + std::forward<_Callable>(__f), + std::forward<_Args>(__args)...))); +} +``` + +`__f`为我们传入的lambda,这里通过bind将lambda与参数绑定在一起,随后传给`_M_make_routine`,便构造出`_Impl`。 + +```cpp +template + shared_ptr<_Impl<_Callable>> + _M_make_routine(_Callable&& __f) + { + // Create and allocate full data structure, not base. + return std::make_shared<_Impl<_Callable>>(std::forward<_Callable>(__f)); + } +``` + +而`_Impl`里面存的是刚才bind的返回结果`_M_func`。 + +```cpp +template + struct _Impl : public _Impl_base + { + _Callable _M_func; + + _Impl(_Callable&& __f) : _M_func(std::forward<_Callable>(__f)) + { } + + void + _M_run() { _M_func(); } + }; +``` + +随后`_M_make_routine`创建好的`shared_ptr<_Impl<_Callable>>`传递给`_M_start_thread`。 + +## 2._M_start_thread + +`_M_start_thread`会调用`__gthread_create`,在这里会传递`execute_native_thread_routine`。 + +```cpp +void + thread::_M_start_thread(__shared_base_type __b) + { + if (!__gthread_active_p()) +#if __EXCEPTIONS + throw system_error(make_error_code(errc::operation_not_permitted), + "Enable multithreading to use std::thread"); +#else + __throw_system_error(int(errc::operation_not_permitted)); +#endif + + __b->_M_this_ptr = __b; + int __e = __gthread_create(&_M_id._M_thread, + &execute_native_thread_routine, __b.get()); + if (__e) + { + __b->_M_this_ptr.reset(); + __throw_system_error(__e); + } + } +``` + +关键点1:`execute_native_thread_routine` + +在下面代码中给出了几个信息: + +第一:为何要extern "C" + +第二:调用了`_M_run()` + +```cpp + extern "C" + { + static void* + execute_native_thread_routine(void* __p) + { + thread::_Impl_base* __t = static_cast(__p); + thread::__shared_base_type __local; + __local.swap(__t->_M_this_ptr); + + __try + { + __t->_M_run(); + } + __catch(const __cxxabiv1::__forced_unwind&) + { + __throw_exception_again; + } + __catch(...) + { + std::terminate(); + } + + return 0; + } + } // extern "C" +``` + +关键点2:`__gthread_create` + +调用了pthread_create + +```cpp +static inline int +__gthread_create (__gthread_t *__threadid, void *(*__func) (void*), + void *__args) +{ + return __gthrw_(pthread_create) (__threadid, NULL, __func, __args); +} +``` + +好了,我们来梳理一下上面的流程,当我们构造std::thread时,传入了自定义的callback(lambda)执行我们的业务逻辑,std::thread构造函数会发起pthread_create的调用,同时把execute_native_thread_routine以C语言风格传递给pthread_create,在执行pthread_create会执行execute_native_thread_routine,那么里面的`_M_run()`便会执行,这便是对应我们的lambda函数调用。 + +本节对线程的调用有了更近一步的认识,你学到了吗,欢迎分享。 \ No newline at end of file diff --git a/site_extras/cpp-concurrency-std-thread-jthread-guide.md b/site_extras/cpp-concurrency-std-thread-jthread-guide.md new file mode 100644 index 0000000..b740f34 --- /dev/null +++ b/site_extras/cpp-concurrency-std-thread-jthread-guide.md @@ -0,0 +1,742 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-concurrency/std-thread-jthread-guide/) 的文章,由站点同步脚本导出。 + +# C++ std::thread 与 jthread + +## 全面解析thread 与 jthread + +## 1.准备工作 + +本文的内容基于gcc-13的源码进行展开,讲详细讲解C++11 std::thread的使用与内核实现、讲解如何使用pthread + thread混用,讲解C++20 jthread的用法与内核实现。 + +如果说你是一个多线程开发的小白/大神,本篇文章都非常的适合学习,既会包含基础的理论知识,还会包含丰富的实践与内核代码揭秘。 + +源码剖析版本:gcc-13 + +> https://github.com/gcc-mirror/gcc/tree/releases/gcc-13 + +大纲如下: + +[TOC] + +## 2.C++11的thread + +### 2.1 基本使用 + +创建一个thread,然后传入自己的执行函数,参数等。 + +这里的result可以使用atomic 也可以使用promise。 + +基本使用中一般需要注意t.join,让主线程等待子线程,它会阻塞当前线程,直到调用 join() 的线程执行完成。 + +```cpp +std::atomic result(0); + +void threadFunction(int a, int b) { + result = a + b; + std::cout << "Child thread finished its work.\n"; +} + +int main() { + int a = 5; + int b = 7; + + std::thread t(threadFunction, a, b); + + t.join(); + std::cout << "Result computed by the child thread: " << result << std::endl; + + return 0; +} +``` + +如果你没有在线程上指定 join() 或 dettach() ,则会导致运行时错误,因为主/当前线程将完成其执行,而创建的其他线程仍将运行。 + +示例:去掉上面的t.join(),你的程序将会崩溃。 + +``` +Result computed by the child thread: 0 +Child thread finished its work. +terminate called without an active exception +[1] 52814 abort ./a.out +``` + +### 2.2 STL实现 + +大家可以去看std_thread.h文件,来跟我一起阅读内核实现。 + +代码路径:gcc/libstdc++-v3/include/bits/std_thread.h + +#### 2.2.1 与pthread关联 + +首先它是一个类thread,然后有一个比较重要的类型,像我的Linux系统上面这个宏是打开的,默认一般是`__gthread_t`,而这个通常是一个pthread_t。 + +```cpp +#ifdef _GLIBCXX_HAS_GTHREADS + using native_handle_type = __gthread_t; +#else + using native_handle_type = int; +#endif +``` + +即: + +``` +typedef pthread_t __gthread_t; +``` + +所以我们可以得到第一个结论:底层实现基于linux的pthread实现。 + +#### 2.2.2 内部id + +thread类,有一个私有成员叫做`_M_id`。 + +```cpp +private: + id _M_id; +``` + +其类型是哪部的一个类,再往里面深入就是native_handle_type。 + +```cpp +class id +{ + native_handle_type _M_thread; + +public: + id() noexcept : _M_thread() { } + + explicit + id(native_handle_type __id) : _M_thread(__id) { } + +} +``` + +那再结合上面的内容,可以得到: + +```cpp +private: + pthread_t _M_id; +``` + +是不是很熟悉,我们得到了第二个知识点:thread内部维护了一个id,在linux上默认是pthread_t。 + +#### 2.2.3 是否可连接 + +如果一个线程处于 "joinable" 状态,那么在它执行完成之前,程序可以调用 `join()` 函数来等待该线程的执行完成。 + +joinable判断依据是: + +```cpp +_M_id != id() +``` + +基于前面的内容,我们可以推导出: + +```cpp +pthread_t _M_id; +pthread_t null_thread = pthread_t(); +_M_id != null_thread; +``` + +即:joinable判断依据是当前线程是否不等于默认的线程id(0)。 + +join方法非常的重要,我们通常需要使用它,那它的实现原理简单来说就是如下: + +- 是否可joinable + - 是,调用pthread_join(这里的gthread实际上会调用到pthread的接口) + - 否或者上面join有错误,跑出异常EINVAL或者join错误。 +- 设置为不可joinable,即:设置内部的线程id为0 + +```cpp +thread::join() +{ + int __e = EINVAL; + + if (_M_id != id()) // 是否可joinable + __e = __gthread_join(_M_id._M_thread, 0); + + if (__e) + __throw_system_error(__e); + + _M_id = id(); // _M_id = pthread_t(),设置为0 +} +``` + +在join之后,如果我们再join,此时便会抛出上面EINVAL异常,即:无效参数。 + +``` +terminate called after throwing an instance of 'std::system_error' + what(): Invalid argument +``` + +#### 2.2.4 分离 + +跟join类似,不过相反,告诉程序你不再关心该线程的执行了,实现逻辑除了调用接口不一样,其他地方都一致,不赘述了。 + +detach重使用的是pthread_detach。 + +```cpp +void +thread::detach() +{ + int __e = EINVAL; + + if (_M_id != id()) + __e = __gthread_detach(_M_id._M_thread); + + if (__e) + __throw_system_error(__e); + + _M_id = id(); +} +``` + +#### 2.2.5 构造与析构 + +##### 2.2.5.1 构造 + +使用: + +``` +thread t1; +``` + +默认构造内核代码: + +```cpp +thread() noexcept = default; +``` + +带参数的构造使用: + +```cpp +std::thread t2(threadFunction, a, b); +``` + +带参数构造内核代码: + +```cpp +template >> +explicit thread(_Callable &&__f, _Args &&...__args) {} +``` + +内部实现是: + +```cpp +_M_start_thread(_State_ptr(new _State_impl<_Wrapper>( + std::forward<_Callable>(__f), std::forward<_Args>(__args)...)), + _M_thread_deps_never_run); +``` + +可以看到核心函数是`_M_start_thread`,这里面的代码简化为: + +```cpp +const int err = __gthread_create(&_M_id._M_thread, + &execute_native_thread_routine, + state.get()); +if (err) + __throw_system_error(err); +``` + +而`__gthread_create`可以理解为是pthread_create,那么是不是非常的清楚了? + +注意到这里有一个细节就是传递给pthread_create的参数重func是execute_native_thread_routine,上面转化为pthread_create为: + +```cpp +pthread_create (&_M_id._M_thread, NULL, &execute_native_thread_routine, state.get()); +``` + +而execute_native_thread_routine是一个extern "C" 包裹的函数,使 C++ 中的函数名称具有 C 链接。 + +所以上面构造函数逻辑非常清晰了,就是调用pthread_create初始化内部线程id,那么这里有一个疑问,**用户传递的函数与参数怎么处理呢?** + +这就引入了State,细心的朋友注意到上面pthread_create的最后一个参数是通常的函数参数:`state.get()`,函数是execute_native_thread_routine。 + +```cpp +static void* +execute_native_thread_routine(void* __p) +{ + thread::_State_ptr __t{ static_cast(__p) }; + __t->_M_run(); + return nullptr; +} +``` + +`_State`是一个接口,其实现是`_State_impl`,可以看到参数与函数是转发到了这里面,然后通过`_M_run`去执行用户传递的函数,执行的时候是调用`_Callable`的operator(),在前面我们知道创建的时候是带了`_Wrapper`,所以`_Callable`等于`_Wrapper`,而`__Wrapper`又是一个`_Call_wrapper`别名,会将用户的函数与参数传递进去。 + +```cpp +using _Wrapper = _Call_wrapper<_Callable, _Args...>; + +struct _State +{ + virtual ~_State(); + virtual void _M_run() = 0; +}; +using _State_ptr = unique_ptr<_State>; + + +template +struct _State_impl : public _State +{ + _Callable _M_func; + + template + _State_impl(_Args &&...__args) + : _M_func(std::forward<_Args>(__args)...) + { + } + + void + _M_run() { _M_func(); } +}; +``` + +所以到这里,我们可以看一下 + +```cpp +template +using _Call_wrapper = _Invoker::type...>>; +``` + +相当于将上面_Call_wrapper的所有参数去掉引用和修饰之后作为tuple传递进去,即: + +```cpp +_Invoker +``` + +然后最终传递给`std::__invoke`。 + +所以,假设我写了这么一行代码: + +```cpp +_Call_wrapper invoker(&add, 3, 4); +``` + +它会通过invoke帮你调用add函数。 + +```cpp +invoke(_Fn&& __f, _Args&&... __args) +``` + +当然上面调用的是其内部的`__invoke`,两者没有什么区别,一个是对外的接口,一个是对内的。 + +经过上面的讲解,我们可以小结一下: + +- 带参数的构造函数会创建tid,通过pthread_create +- 传递给pthread_create的函数是一个包裹的函数,内部使用State去驱动 +- 用户通过将函数、参数传递给thread,最终调用这个函数是通过`std::__invoke`来驱动起来。 + +##### 2.2.5.2 拷贝与移动 + +thead禁止了拷贝构造、拷贝赋值,只支持移动构造与移动赋值。 + +```cpp +thread(const thread &) = delete; +thread &operator=(const thread &) = delete; +``` + +移动构造 + +```cpp +thread(thread &&__t) noexcept +{ + swap(__t); +} + +void +swap(thread &__t) noexcept +{ + std::swap(_M_id, __t._M_id); +} +``` + +移动赋值 + +```cpp +thread &operator=(thread &&__t) noexcept +{ + if (joinable()) + std::__terminate(); + swap(__t); + return *this; +} +``` + +##### 2.2.5.3 析构 + +析构的原则是你创建的线程资源得消费/释放掉,而一个线程如果是可joinable,那么它被pthread_create了,但是还没有被使用,此时会直接terminate。 + +```cpp +~thread() +{ + if (joinable()) + std::__terminate(); +} +``` + +#### 2.2.6 其他成员 + +例如:获取线程内部的线程id、线程的id内部的`_M_thread`。 + +```cpp +id +get_id() const noexcept +{ return _M_id; } + +native_handle_type +native_handle() +{ return _M_id._M_thread; } +``` + +#### 2.2.7 面试考点 + +**1.构造与析构** + +所以这里有一个提问:下面代码有什么问题吗? + +```cpp +// test1.cc +int main() { + thread t1; +} + +// test2.cc +int main() { + std::thread t2(threadFunction, a, b); +} +``` + +其答案是:test1正常运行,test2 crash! + +原因是:thread的默认构造函数不做任何事情,而带参数的构造函数里面会调用`__gthread_create`(即:pthread_create)创建线程,即初始化了内部的`_M_id._M_thread`,在析构的时候会判断joinable,此时的`_M_id`与id()是不相等的,即:joinable = true,直接std::__terminate()此时crash了。 + +**2.移动赋值** + +假设现在有下面两个函数f1、f2,这两个函数可以正常运行吗?为什么? + +```cpp +void f1() { + std::thread t1(tfunc); + std::thread t2; + t2 = std::move(t1); + t2.join(); +} +void f2() { + std::thread t1(tfunc); + std::thread t2(tfunc); + t2 = std::move(t1); + t2.join(); +} +``` + +其答案是:f1正常运行,f2 crash! + +原因是:`f1`中t2使用了默认构造函数(不做任何事情),所以`t2`本身是非joinable的,可以安全使用移动赋值。但是,`f2`的t2是可joinable的(因此构造函数中初始化了tid),而移动赋值时会判断是否joinable,如果joinable,那么直接terminate,这其实比较好理解,因为t2本身的线程被创建了,还没有消费使用,就给swap出去了,那如何修复这个问题呢? + +其实很简单:在前面加个join即可: + +```cpp +t2.join(); // 新加的 +t2 = std::move(t1); +t2.join(); +``` + +可能有些人觉得t2搞了两次join难道没问题吗,因此此时第二次的t2实际上已经是t1了,所以可以看作是: + +```cpp +t2.join(); +t1.join(); +``` + + + +## 3.C++20的jthread + +自C++20之后引入了`std::jthread`,它可以自动join启动的线程,并发出中断信号。 + +### 3.1 基本使用 + +如果我们使用前面的thread,下出下面这个代码会core掉,因为在析构函数中会去判断是否joinable,此时会terminate。 + +```cpp +std::thread t{ [] {std::cout << "hello std::thread" << std::endl; } }; + +std::cout << "t.joinable(): " << t.joinable() << std::endl; +``` + +如果使用了C++20的jthread,便不会有这个问题,例如: + +```cpp +std::jthread tj{ [] {std::cout << "hello std::jthread" << std::endl; } }; + +std::cout << "tj.joinable(): " << tj.joinable() << std::endl; +``` + +此时你会发现可以正常运行! + +### 3.2 STL实现 + +jthread是在thread里面,其实现基于前面的thread,源码位置:gcc/libstdc++-v3/include/std/thread + +到底有什么秘密呢?我们一起来揭秘。 + +#### 3.2.1 与thread关联 + +jthread与thread息息相关,可以说基于thread实现,其中最核心的成员就是thread类型,而内部的id与native_handle_type类型均使用了thread内部的。 + +```cpp +using id = thread::id; +using native_handle_type = thread::native_handle_type; + +thread _M_thread; +``` + +#### 3.2.2 stop_token与stop_source + +jthread内部使用了 std::stop_token 增强控制。std::stop_token 是一种向线程执行发送停止请求的机制。 它作为请求运行,如果在线程执行中正确处理,则允许其被终止。 + +内部使用了std::stop_source成员来维护。 + +```cpp +stop_source _M_stop_source; +``` + +下面我们一起来看看STL的实现。 + +**1.stop_source** + +首先stop_source内部会维护一个state,叫做: + +```cpp +stop_token::_Stop_state_ref _M_state; +``` + +这个里面便是: + +``` +_Stop_state_t* _M_ptr +``` + +因为一个stop_source可以被不同的线程所持有,所以它可以是共享的,因此内部是通过一个引用来做的,所以你可能会在源码里面看到`_Stop_state_ref`,它来负责管理整个state的生命周期,所有权问题。其共享的实现是通过底层的`_Stop_state_t`实现,内部有两个成员非常重要: + +```cpp +using value_type = uint32_t; +std::atomic _M_owners{1}; +std::atomic _M_value{_S_ssrc_counter_inc}; +``` + +一个是`_M_owners`,管理所有权的;`_M_value`负责管理什么时候停止。 + +我们平时用的三个接口分别是: + +- stop_possible +- stop_requested +- request_stop + +底层便会给`_M_value`设置上对应的bit + +```cpp +static constexpr value_type _S_stop_requested_bit = 1; +static constexpr value_type _S_locked_bit = 2; +static constexpr value_type _S_ssrc_counter_inc = 4; +``` + +当用户请求停止:request_stop,此时会设置上`_S_stop_requested_bit`,如果用户调用了stop_requested,那么也是去查看内部的`_M_value`是否设置上`_S_stop_requested_bit`。 + +`_S_locked_bit`与stop_possible接口有关,如果已经发出了停止请求或者仍然存在可以发出停止请求,那么stop_possible会返回true。 + +另外一个是`_S_ssrc_counter_inc`,这个是默认值,在stop_source的构造/拷贝构造时会给value加上`_S_ssrc_counter_inc`。 + +**2.stop_token** + +像上面讲到的`_Stop_state_ref`所有内容基本都在stop_token里面,它的内部也是维护了一个state,叫做: + +``` +_Stop_state_ref _M_state; +``` + +实现细节与上面一致,就不赘述了。那么它与stop_source的区别是什么呢? + +这里引用cppreference的解释:stop_token 类提供了检查是否已为其关联的 std::stop_source 对象发出或可以发出停止请求的方法。 它本质上是相关停止状态的线程安全“视图”。 + +> https://en.cppreference.com/w/cpp/thread/stop_token + +**3.stop_callback** + +stop_callback与stop_token的关系是stop_callback会注册到stop_token里面,stop_token内部会维持一个双向链表结构来对callback进行删除与新增,插入callback采用的是头插法。 + +callback触发时机有两块: + +- 在request_stop时触发 + +callback提前注册到stop_token里面,那么在执行一段时间,用户发起请求,此时会触发callback。 + +```cpp +auto onStop = []() { + std::cout << "Timer has stopped." << std::endl; +}; + +// Register the onStop callback with stop_callback +std::stop_callback callback(source.get_token(), onStop); +// do something for example: sleep + +// stop +source.request_stop(); // invoke callback +``` + +- 注册时立即触发 + +stop_token已经要求停止,此时callback在注册时会立即触发,因为线程已经执行完了,此时应该执行回调。 + +```cpp +thread.request_stop(); +std::stop_callback callback(stopToken, []() { + std::cout << "Callback executed" << std::endl; +}); // invoke callback +``` + +具体STL中的实现是`_M_register_callback`会调用`__cb->_M_run()`,此时会调用callback函数。 + +#### 3.2.3 构造与析构 + +##### 3.2.3.1 构造 + +- 默认构造 + +默认构造会初始化一个空的stopstate,内部tid不做维护,行为跟thread一致。 + +```cpp +jthread() noexcept + : _M_stop_source{nostopstate} + { } +``` + +- 带参数构造 + +初始化`_M_thread`,它是通过`_S_create`然后调用`_S_create_pmf`创建一个thread,然后给`_M_thread`,这样内部的thread变被构造好了。 + +```cpp +template, + jthread>>> + explicit + jthread(_Callable&& __f, _Args&&... __args) + : _M_thread{_S_create(_M_stop_source, std::forward<_Callable>(__f), + std::forward<_Args>(__args)...)} + { } +``` + +##### 3.2.3.2 拷贝与移动 + +拷贝构造与拷贝赋值被禁用掉了,行为跟thread一致! + +``` +jthread(const jthread&) = delete; +jthread& operator=(const jthread&) = delete; +``` + +移动构造默认,移动赋值使用swap。 + +```cpp +jthread(jthread&&) noexcept = default; + +jthread& +operator=(jthread&& __other) noexcept +{ + std::jthread(std::move(__other)).swap(*this); + return *this; +} + +void +swap(jthread& __other) noexcept +{ + std::swap(_M_stop_source, __other._M_stop_source); + std::swap(_M_thread, __other._M_thread); +} +``` + +##### 3.2.3.3 析构 + +析构决定了jthread在一开始的行为,为何我们不用手动join,是因为在析构的时候,它会去判断是否可join,如果joinable,那么用户没有join过,析构函数帮你调用一下join,同时会在之前检查request_stop。 + +```cpp +~jthread() +{ + if (joinable()) + { + request_stop(); + join(); + } +} +``` + +##### 3.2.3.4 其他成员 + +其他成员基本上是在thread上进行操作,然后对外暴露了一个接口,例如: + +```cpp +[[nodiscard]] bool +joinable() const noexcept +{ + return _M_thread.joinable(); +} + +join() +{ + _M_thread.join(); +} + +void +detach() +{ + _M_thread.detach(); +} + +[[nodiscard]] id +get_id() const noexcept +{ + return _M_thread.get_id(); +} + +[[nodiscard]] native_handle_type +native_handle() +{ + return _M_thread.native_handle(); +} + +[[nodiscard]] static unsigned +hardware_concurrency() noexcept +{ + return thread::hardware_concurrency(); +} +``` + +剩下一些是对stop_source的操作: + +```cpp +[[nodiscard]] stop_source +get_stop_source() noexcept +{ + return _M_stop_source; +} + +[[nodiscard]] stop_token +get_stop_token() const noexcept +{ + return _M_stop_source.get_token(); +} + +bool request_stop() noexcept +{ + return _M_stop_source.request_stop(); +} +``` + +### 3.3 示例 + +cppreference中这个例子非常不错,在示例中用户工作函数传递了stop_source作为参数,在内部可以通过判断stop_requested(),如果停止了,我们可以做一些释放资源的操作,还是非常有用的。 + +触发的条件就是主线程执行了request_stop。 + +> https://en.cppreference.com/w/cpp/thread/stop_source \ No newline at end of file diff --git a/site_extras/cpp-guides-cpp-string-split-methods.md b/site_extras/cpp-guides-cpp-string-split-methods.md new file mode 100644 index 0000000..edaf6e0 --- /dev/null +++ b/site_extras/cpp-guides-cpp-string-split-methods.md @@ -0,0 +1,98 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-guides/cpp-string-split-methods/) 的文章,由站点同步脚本导出。 + +# C++ 字符串六种分割方法 + +通常我们在开发的时候会对一个长字符串依据分隔符进行分割,从而得到我们想要的字符串,大家都说C++字符串分割并没有那么方便,今天来总结一下5种方法分割字符串。 + +包含了从C++11到C++20的写法,以及C风格。 + +为了方便后续文章的讲解,我们先定义一个函数原型: + +- 参数:两个字符串,一个表示原字符串,另一个表示分隔符 + +- 返回值:字符串数组 + +注:懒人版,可以从星球获取完整代码。 + +## 1.find分割 + +第一种方法便是大家常用的先找到分割符的位置,然后划分字符串,添加到数组中,于是有了下面的代码。 + +```cpp +nt start = 0; +int end = s.find(delim); +std::vector result; +while (end != std::string::npos) { + result.push_back(s.substr(start, end - start)); + start = end + delim.size(); + end = s.find(delim, start); +} + +result.push_back(s.substr(start, end - start)); +``` + +这种办法是否还能进行优化呢? + +答案是肯定的,为了使得代码看起来更加优雅,我们可以每次删除前面已经输出的子串,然后find逻辑一致。 + +```cpp +while (end != std::string::npos) { + result.push_back(s.substr(0, end)); + s.erase(0, end + delim.size()); + end = s.find(delim); +} +``` + +## 2.C++20 Ranges与Views分割 + +C++20中我们可以使用 `std::ranges::views::split()` 创建一个分隔符的视图,然后使用transform,将每个子视图转换为 `std::string`。 + +```cpp +auto split_transform_view = s | std::ranges::views::split(delim) | + std::ranges::views::transform([](auto&& str) { + return std::string(str.begin(), str.end()); + }); +``` + +是不是很简单,可能一些人没看懂,诶关系去看之前的C++20文章吧。 + +## 3.正则分割 + +使用 `std::regex` 对象构造分隔符模式,使用`std::sregex_token_iterator` 遍历由正则表达式模式分隔的子字符串,-1表示提取不匹配的字符串,即除了分割符的字符串。 + +```cpp +std::regex r(delim); +return {std::sregex_token_iterator(s.begin(), s.end(), r, -1), + std::sregex_token_iterator()}; +``` + +## 4.istringstream分割 + +getline每次只能传递一个分隔符,于是分隔之后,需要继续跳过后续的分隔符。 + +```cpp +std::istringstream ss(s); +while (std::getline(ss, token, *(it = delim.begin()))) { + result.push_back(token); + it++; + + while (it != delim.end() && ss.peek() == *(it)) { + it++; + ss.get(); + } +} +``` + +## 5.C风格的strtok分割 + +使用 `strtok()` 将字符串分割成标记,然后while循环依次提取即可。 + +```cpp +token = strtok((char*)s.c_str(), delim.c_str()); +while (token != NULL) { + result.push_back(token); + token = strtok(NULL, delim.c_str()); +} +``` + +以上便是常用的几种方法,你还有其他方法吗?欢迎留言区见,本节懒人版可以从星球获取完整代码。 \ No newline at end of file diff --git a/site_extras/cpp-guides-cpython-extension-notes.md b/site_extras/cpp-guides-cpython-extension-notes.md new file mode 100644 index 0000000..9f80a2f --- /dev/null +++ b/site_extras/cpp-guides-cpython-extension-notes.md @@ -0,0 +1,148 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-guides/cpython-extension-notes/) 的文章,由站点同步脚本导出。 + +# CPython 扩展开发笔记 + +本篇文章将会围绕最近给Apache提的一个feature为背景,展开讲讲CPython遇到的问题,以及尝试自己从0写一个库出来,代码也已经放星球了,感兴趣的同学可以去下载学习。 + +## 0.背景 + +最近在给apache arrow提的一个feature因为C++接口的变动引发其他语言的接口变动,一些测试也跟着需要修复。 + +像PyArrow熟悉的人应该一点也不陌生,这次接口变动也需要修改这个库,因为是在一个仓库里的,不然ci过不了。而PyArrow的实现是通过CPython实现的,之前也没特别学习CPython,改出了一堆问题,其中遇到两个问题比较重要,这里记录一下。 + +**问题1:初始化函数里面不支持其他类的默认构造。** + +示例: + +``` + def __init__(self, mode="only_valid", filter=Expression._scalar(True)): + pass +``` + +报错: + +``` +TypeError: descriptor '_scalar' for 'pyarrow._compute.Expression' objects doesn't apply to a 'bool' object +``` + +可以看到没识别出来,实际情况是Expression._scalar(True)合法的,我们看里面的实现: + +```python +@staticmethod +def _scalar(value): + cdef: + Scalar scalar + + if isinstance(value, Scalar): + scalar = value + else: + scalar = lib.scalar(value) + + return Expression.wrap(CMakeScalarExpression(scalar.unwrap())) +``` + +可以看到,里面支持正常的bool类型,我怀疑这是cpython的限制,于是改为下面这种方式就可以了: + +```python +def __init__(self, mode="only_valid", filter=None): + if filter is None: + filter = Expression._scalar(True) +``` + +**问题2:定义顺序** + +当我使用后面创建的`_true`,每次传递进去的默认值是空,这个比较好理解,因为最后编译好了会翻译为一个`xxx.cpp`文件,根据C++规则前面读到的自然就是空了。 + +```python +def __init__(self, mode="only_valid", filter=_true): + pass + + +cdef CExpression _true = CMakeScalarExpression( + make_shared[CBooleanScalar](True) +) +``` + +好了,基于以上背景,我自己也想写一个例子出来,例如:使用C++写一个类,封装sort和sum,然后使用Python调用。 + +## 1.CPython完整例子 + +1. 创建一个.h文件 + +```cpp +void sort(std::vector& nums) { + std::sort(nums.begin(), nums.end()); +} +int sum(std::vector& nums) { + int sum = 0; + for (int num : nums) { + sum += num; + } + return sum; +} +``` + +2) 创建foo.pyx + +重要点:上面vector需要: + +``` +from libcpp.vector cimport vector +``` + +然后去定义一个class,调用C++的接口。 + +``` +cdef class PyFoo: + cdef Foo* f + + def __cinit__(self): + self.f = new Foo() + + def __dealloc__(self): + del self.f + + def sort(self, nums): + cdef vector[int] c_nums = nums + self.f.sort(c_nums) + + def sum(self, nums): + cdef vector[int] c_nums = nums + return self.f.sum(c_nums) +``` + +3. 创建setup.py文件 + +```python +ext = Extension('Foo', sources=["foo.pyx"], language="c++", include_dirs=[numpy.get_include()]) + +setup(name="Foo", ext_modules = cythonize([ext])) +``` + +4. 运行 + +``` +python3 setup.py build_ext --inplace +``` + +最后,可以写一个测试脚本去使用自己写的python接口。 + +```python +import Foo + +f = Foo.PyFoo() +nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5] +f.sort(nums) +print("Sorted nums:", nums) +print("Sum of nums:", f.sum(nums)) +``` + +CPython在一些项目中使用挺多的,学习起来吧~ + +运行: + +``` +➜ cpython_examples python3 test.py +Sorted nums: [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5] +Sum of nums: 44 +``` \ No newline at end of file diff --git a/site_extras/cpp-guides-curl-cpp-notes.md b/site_extras/cpp-guides-curl-cpp-notes.md new file mode 100644 index 0000000..e68b353 --- /dev/null +++ b/site_extras/cpp-guides-curl-cpp-notes.md @@ -0,0 +1,271 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-guides/curl-cpp-notes/) 的文章,由站点同步脚本导出。 + +# libcurl C++ 开发备忘 + +代码风格统一有助于团队协作与代码review,业界用的比较多的是google的风格,本节来点不一样的,看看咱们平时用的比较多的工具curl团队的代码规范是什么样子的。 + +curl简单说一下,它在命令行或脚本中传输数据非常有用,大家也肯定用过,其源码实现可以在github上查看 + +> https://github.com/curl/curl + +下面我们来从几个方面看看curl的C语言开发规范。 + +[TOC] + +## 命名 + +新的函数和变量名称应该是逻辑的、可理解的,并且根据它们的用途进行命名。 + +文件局部函数应该被声明为静态的,建议是小写字母名称。 + +## 缩进 + +只使用空格进行缩进,不使用制表符。每个新的开放大括号使用两个空格。 + +``` +if(something_is_true) { + while(second_statement == fine) { + moo(); + } +} +``` + +## 注释 + +由于编写的是 C89 代码,不允许使用 **//** 注释。它们直到 C99 标准才被引入。只使用 **/\* 注释 \*/**。 + +``` +/* 这是一个注释 */ +``` + +## 长行 + +curl 中的源代码永远不应该超过 79 列,即使在现代大屏幕和高分辨率屏幕时代,仍然有两个原因要保持这一点: + +1. 较窄的列比较宽的列更容易阅读。有一个原因是报纸几十年甚至几个世纪来一直使用列。 +2. 较窄的列允许开发人员更容易地在不同的窗口中并排显示多个代码片段。它允许在同一个屏幕上并排显示两个或三个源代码窗口 - 以及多个终端和调试窗口。 + +## 大括号 + +在 if/while/do/for 表达式中,我们将开放大括号写在与关键字同一行,然后将闭合大括号设置在与初始关键字相同缩进级别的同一行。就像这样: + +``` +if(age < 40) { + /* 显然是年轻人 */ +} +``` + +如果它们只包含一个一行语句,则可以省略大括号: + +``` +if(!x) + continue; +``` + +对于函数,开放大括号应该写在单独的一行上: + +``` +int main(int argc, char **argv) +{ + return 1; +} +``` + +## 'else' 在下一行 + +在使用大括号添加 **else** 子句到条件表达式时,我们将其添加到关闭大括号后的新行。就像这样: + +``` +if(age < 40) { + /* 显然是年轻人 */ +} +else { + /* 可能是脾气暴躁的 */ +} +``` + +## 括号前不加空格 + +在使用 if/while/do/for 表达式时,关键字与开放括号之间不应有空格。就像这样: + +``` +while(1) { + /* 永远循环 */ +} +``` + +## 使用布尔条件 + +在 if/while 条件中,我们更喜欢测试条件值,如布尔值与 TRUE 或 FALSE、指针与 NULL 或 != NULL 以及整数与零或非零,而不是: + +``` +result = do_something(); +if(!result) { + /* 出现了问题 */ + return result; +} +``` + +## 条件中不要赋值 + +为了增加可读性并减少条件的复杂性,避免在 if/while 条件中进行变量赋值。不赞成这种风格: + +``` +if((ptr = malloc(100)) == NULL) + return NULL; +``` + +而是鼓励更清晰地表达: + +``` +ptr = malloc(100); +if(!ptr) + return NULL; +``` + +## 新块在新行上 + +永远不会在同一行上写多个语句,即使是短的 if() 条件也不例外。 + +``` +if(a) + return TRUE; +else if(b) + return FALSE; +``` + +永远不要这样: + +``` +if(a) return TRUE; +else if(b) return FALSE; +``` + +## 运算符周围加空格 + +请在 C 表达式中运算符的两侧使用空格。后缀 **(), [], ->, ., ++, --** 和一元 **+, -, !, ~, &** 操作符除外,它们不应该有空格。 + +示例: + +``` +bla = func(); +who = name[0]; +age += 1; +true = !false; +size += -2 + 3 * + + (a + b); +ptr->member = a++; +struct.field = b--; +ptr = &address; +contents = *pointer; +complement = ~bits; +empty = (!*string) ? TRUE : FALSE; +``` + +## 返回值不加括号 + +在 'return' 语句中不加额外的括号: + +``` +int works(void) +{ + return TRUE; +} +``` + +## sizeof 参数加括号 + +在代码中使用 sizeof 运算符时,我们更喜欢在其参数周围加上括号: + +``` +int size = sizeof(int); +``` + +## 列对齐 + +一些语句不能在单行上完成,因为行太长、语句太难读,或者是由于上述其他风格指南。在这种情况下,语句跨越多行。 + +如果一个连续行是表达式或子表达式的一部分,那么你应该在适当的列上对齐,以便能够清楚地知道它是语句的哪一部分。运算符不应该起始于连续行。在其他情况下,遵循 2 个空格的缩进指南。以下是来自 libcurl 的一些示例: + +``` +if(Curl_pipeline_wanted(handle->multi, CURLPIPE_HTTP1) && + (handle->set.httpversion != CURL_HTTP_VERSION_1_0) && + (handle->set.httpreq == HTTPREQ_GET || + handle->set.httpreq == HTTPREQ_HEAD)) + /* 没有要求 HTTP/1.0 并且是 GET 或 HEAD 请求 */ + return TRUE; +``` + +如果没有括号,使用默认缩进: + +``` +data->set.http_disable_hostname_check_before_authentication = + (0 != va_arg(param, long)) ? TRUE : FALSE; +``` + +函数调用时使用开放括号: + +``` +if(option) { + result = parse_login_details(option, strlen(option), + (userp ? &user : NULL), + (passwdp ? &passwd : NULL), + NULL); +} +``` + +与 "current open" 括号对齐: + +``` +DEBUGF(infof(data, "Curl_pp_readresp_ %d bytes of trailing " + "server response left\n", + (int)clipamount)); +``` + +## 平台相关代码 + +使用 **#ifdef HAVE_FEATURE** 进行条件代码。我们避免在 #ifdef 行中检查特定操作系统或硬件。HAVE_FEATURE 应该由 configure 脚本为类 Unix 系统生成,并且它们在其他系统的 `config-[system].h` 文件中硬编码。 + +我们还鼓励在 libcurl 构建时,使用可能为空或定义为常量的宏/函数,以使代码无缝。就像这个例子,其中 **magic()** 函数根据构建时的条件不同而工作: + +``` +#ifdef HAVE_MAGIC +void magic(int a) +{ + return a + 2; +} +#else +#define magic(x) 1 +#endif + +int content = magic(3); +``` + +## 不要 typedef 结构体 + +尽管可以使用结构体,但不要对其进行 typedef。使用 `struct name` 的方式来标识它们: + +``` +struct something { + void *valid; + size_t way_to_write; +}; +struct something instance; +``` + +**不要这样**: + +``` +typedef struct { + void *wrong; + size_t way_to_write; +} something; +something instance; +``` + +本文翻译自:https://curl.se/dev/code-style.html + + +!!! note "说明" + 本文为开发备忘,建议结合官方 libcurl 文档与具体项目错误码排查。 \ No newline at end of file diff --git a/site_extras/cpp-guides-how-to-read-open-source-cpp.md b/site_extras/cpp-guides-how-to-read-open-source-cpp.md new file mode 100644 index 0000000..bf83b3b --- /dev/null +++ b/site_extras/cpp-guides-how-to-read-open-source-cpp.md @@ -0,0 +1,100 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-guides/how-to-read-open-source-cpp/) 的文章,由站点同步脚本导出。 + +# 如何阅读开源 C++ 代码 + +## 如何秒懂阅读开源项目代码? + +在实际项目或开源代码中,特别是C代码比较喜欢用宏,那么造成了一个很大问题:阅读代码非常的困难,调试也是另外一个问题,那么给你一个项目带有大量的宏,如何快速理解? + +通常我们在面试的时候会遇到一个非常重要的知识点:gcc编译过程,大家可能对答如流,那么为了区分候选人的水平,引申出:你知道gcc哪些编译参数,各自参数有什么用?预处理阶段会发生什么?一般你会在这个阶段做些什么,对你有何帮助? + +本节就一个开源项目为例,如何快速秒懂代码! + +下面是一段libev的代码,其中有若干个宏,比如: + +- EV_FREQUENT_CHECK +- assert +- `EV_P_` +- ANHE_w +- ANHE_at_cache + +```cpp +void noinline +ev_timer_start (EV_P_ ev_timer *w) EV_THROW +{ + if (expect_false (ev_is_active (w))) + return; + + ev_at (w) += mn_now; + + assert (("libev: ev_timer_start called with negative timer repeat value", w->repeat >= 0.)); + + EV_FREQUENT_CHECK; + + ++timercnt; + ev_start (EV_A_ (W)w, timercnt + HEAP0 - 1); + array_needsize (ANHE, timers, timermax, ev_active (w) + 1, EMPTY2); + ANHE_w (timers [ev_active (w)]) = (WT)w; + ANHE_at_cache (timers [ev_active (w)]); + upheap (timers, ev_active (w)); + + EV_FREQUENT_CHECK; + + /*assert (("libev: internal timer heap corruption", timers [ev_active (w)] == (WT)w));*/ +} +``` + +看起来是不是一愣一愣的,这段代码引出几个问题: + +- 函数参数是几个? +- 函数类型是? +- 代码中ev_at是啥玩意? + +为了秒懂代码,我们需要使用`-E`来得到预处理阶段宏展开之后的代码,方便我们理解与阅读。 + +展开之后,我们会得到: + +```cpp +void __attribute__ ((__noinline__)) +ev_timer_start (struct ev_loop *loop, ev_timer *w) +{ + if (__builtin_expect ((!!((0 + ((ev_watcher *)(void *)(w))->active))),(0))) + return; + + ((WT)(w))->at += ((loop)->mn_now); + + +# 3881 "ev.c" 3 4 + (( +# 3881 "ev.c" + ("libev: ev_timer_start called with negative timer repeat value", w->repeat >= 0.) +# 3881 "ev.c" 3 4 + ) ? (void) (0) : __assert_fail ( +# 388libev: ev_timer_start called with negative timer repeat value\", w->repeat >= 0.)" +# 3881 "ev.c" 3 4 + , "ev.c", 3881, __PRETTY_FUNCTION__)) +# 3881 "ev.c" + ; + + do { } while (0); + + ++((loop)->timercnt); + ev_start (loop, (W)w, ((loop)->timercnt) + (4 - 1) - 1); + if (__builtin_expect ((!!((((W)(w))->active + 1) > (((loop)->timermax)))),(0))) { int __attribute__ ((__unused__)) ocur_ = (((loop)->timermax)); (((loop)->timers)) = (ANHE *)array_realloc (sizeof (ANHE), (((loop)->timers)), &(((loop)->timermax)), (((W)(w))->active + 1)); ; }; + (((loop)->timers) [((W)(w))->active]).w = (WT)w; + (((loop)->timers) [((W)(w))->active]).at = (((loop)->timers) [((W)(w))->active]).w->at; + upheap (((loop)->timers), ((W)(w))->active); + + do { } while (0); + + +} +``` + +此时我们去理解代码,基本上没啥障碍了,但是还是不够清晰明了,为了突出你与其他候选人之间的差异,我提出几个问题: + +- 上面显示了很多行数,可以去掉吗,怎么解决? +- 上面assert这种系统宏非常的简单好理解,不需要我们展开它,你知道怎么做? +- 在上面这二步展开中可能会出现问题,你知道发生了什么,如何解决? + +好了,本节就先聊这么多,下一节继续分享,欢迎留言区探讨上面的答案! \ No newline at end of file diff --git a/site_extras/cpp-guides-interesting-cpp-code-snippet.md b/site_extras/cpp-guides-interesting-cpp-code-snippet.md new file mode 100644 index 0000000..79ec25d --- /dev/null +++ b/site_extras/cpp-guides-interesting-cpp-code-snippet.md @@ -0,0 +1,45 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-guides/interesting-cpp-code-snippet/) 的文章,由站点同步脚本导出。 + +# 一段有趣的 C++ 代码 + +## 0.导语 + +目标:如何获取参数lambda或者functor的参数个数呢? + +本节将从模版 + `::*` + decay + decltype来实现! + +## 1.实现 + +最终效果: + +```cpp +auto c1 = [](int x, double y) { return x * y; }; +std::cout << argument_count::value << std::endl; +MyClass f; +std::cout << argument_count::value << std::endl; +``` + +输出:2 + +我们思考一下这个问题,需要直到这几个点: + +- 如何定位到functor与lambda的位置? +- 如何获取到这个函数签名的参数数量? + +对于第一个问题,我们可以使用`::*`便可以指向成员声明,对于functor与lambda都是`&std::decay::type::operator())`。简单来说都是获取operator()这个声明的地址。 + +这里使用decay来移除cv限定符,以上述MyClass为例,到这里先通过`std::decay::type`获取到MyClass类型,随后获取对应operator()的地址,然后传递给argument_count_impl。 + +```cpp +template +using argument_count = decltype(argument_count_impl(&std::decay::type::operator())); +``` + +argument_count_impl的实现也比较简单,使用R (F::*)(A...)签名获取到对应的函数,这里使用了可变参数模版,`::*`,sizeof是用来获取参数数量,integral_constant则表示这个编译器常量类型为int,值为sizeof...(A)。 + +```cpp +template +static std::integral_constant argument_count_impl(R (F::*)(A...)); +``` + +基于此,便实现了获取Lambda or Functor参数个数的功能~ \ No newline at end of file diff --git a/site_extras/cpp-guides-print-1-to-100-without-loop.md b/site_extras/cpp-guides-print-1-to-100-without-loop.md new file mode 100644 index 0000000..384a921 --- /dev/null +++ b/site_extras/cpp-guides-print-1-to-100-without-loop.md @@ -0,0 +1,148 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-guides/print-1-to-100-without-loop/) 的文章,由站点同步脚本导出。 + +# C++ 无循环打印 1-100 + +给大家设定一个条件:不使用循环,请打印出1-100个数字。 + +如果使用循环很简单了,那么你能想到多少种方法呢? + +今天来给大家用一些C++的骚操作来写出这种代码,欢迎留言与转发~ + +这道题目其实非常简单,但却涵盖了很多面试考点,其中包括: + +- 模版特化、便特化,如何编写递归模版 +- CRTP +- std::copy、std::iota、std::transform如何使用,各自算法实现 +- 类静态变量 +- 等等 + +## 1.递归 + +1-100可以拆为`5*5*4`,用三个函数去打印它,函数a cout一次,然后变量加1,函数b层层嵌套a重复5次,函数c层层嵌套b重复5次,最后在main函数中对c层层嵌套4次。 + +```cpp +#include + +int a(int n) { + std::cout << n++ << std::endl; + return n; +} + +int b(int n) { return a(a(a(a(a(n))))); } + +int c(int n) { return b(b(b(b(b(n))))); } + +int main() { + c(c(c(c(1)))); + return 0; +} +``` + +## 2.模版重载 + +使用模版与特化来处理,当迭代100时只输出100,否则输出I,并且+1递归处理。 + +```cpp +#include +template +struct JustPrint : public JustPrint { + JustPrint() : JustPrint() { std::cout << I << " "; } +}; + +template <> +struct JustPrint<100> { + JustPrint<100>() { std::cout << 1 << " "; } +}; + +int main() { JustPrint<1> j; } +``` + +## 3.CRTP + +方式3基于方式2,将其改造为crtp编程风格,是不是有点看不懂了,嘿嘿。 + +下面struct可以被替换为class,这里我用类来描述。其实就是两个类,第一个类Print为父类,打印每次的I,并调用子类,子类PrintDerived即成Print,并将自己传递给父类,子类是一个重载类,控制递归终止。 + +```cpp +#include + +template +struct Print { + Print() { + std::cout << I << " "; + Derived::next(); + } +}; + +template +struct PrintDerived : public Print, I> { + static void next() { PrintDerived(); } +}; +template <> +struct PrintDerived<100> : public Print, 100> { + static void next() {} +}; + +int main() { + PrintDerived<1> j; + return 0; +} +``` + +## 4.类静态变量 + +创建一个Test数组,元素为100个,静态变量每次累加。 + +```cpp +#include +using namespace std; + +class Test { + static int i; + + public: + Test() { cout << ++i << endl; } +}; +int Test::i; +int main() { + Test a[100]; + return 0; +} +``` + +## 5.使用算法库 + +使用`std::for_each`简单粗暴。 + +```cpp +int main() { + std::vector l(100); + std::iota(l.begin(), l.end(), 1); + std::for_each(l.begin(), l.end(), [](int& n) { std::cout << n << std::endl; }); + return 0; +} +``` + +## 6.C++20 ranges + +使用C++20的std::views,在std::copy时打印。 + +```cpp +#include +#include +#include +int main() { + auto numbers = std::views::iota(1, 100 + 1); + std::copy(numbers.begin(), numbers.end(), std::ostream_iterator(std::cout, "\n")); +} +``` + +这个方式继续改造,还有一种实现,使用std::transform。 + +```cpp +auto range = std::views::transform(std::ranges::iota_view{1, 101}, [](const auto& x) { + std::cout << x << std::endl; + return x; +}); +std::vector a(range.begin(), range.end()); +``` \ No newline at end of file diff --git a/site_extras/cpp-idioms-avoid-default-constructor.md b/site_extras/cpp-idioms-avoid-default-constructor.md new file mode 100644 index 0000000..6b46f0e --- /dev/null +++ b/site_extras/cpp-idioms-avoid-default-constructor.md @@ -0,0 +1,147 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/avoid-default-constructor/) 的文章,由站点同步脚本导出。 + +# C++ 避免默认构造函数 + +在 C++ 中,有一些容器通过 `resize()` 方法调整大小时,默认构造函数会被调用,这可能并不是我们想要的行为。本文将介绍如何使用自定义分配器来控制这一过程,避免默认构造的调用。 + +示例1: + +```cpp +// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +vec.resize(5); +vec.resize(10); +``` + +你知道此时输出什么吗? + +答案是: + +``` +[0, 1, 2, 3, 4] +[0, 1, 2, 3, 4, 0, 0, 0, 0, 0] +``` + +这里需要明白几点内容: + +**第一:为何resize回去之后的数据变为了0,发生了什么?** + +**第二:如果我想要原来的数据,怎么做?即我想要输出结果是:** + +```cpp +[0, 1, 2, 3, 4] +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + +**第三:如果vector的成员没有默认构造函数会发生什么?** + +**第四:resize有什么限制?** + +那么本节就来研究一下这个强大的功能,如何让resize避免调用默认构造。 + +注:篇幅有限,懒人版本节所有代码更新于星球。 + +## 1.resize剖析 + +为了让本文更加丝滑,我使用了下面示例,禁止Foo的构造,然后vector上面resize它。 + +```cpp +class Foo { + public: + Foo() = delete; + + private: + int a; +}; + +std::vector f; +f.resize(10); +``` + +此时会编译错误,我们可以看到一堆报错信息。 + +第一个问题:可以看到调用栈到了`__uninitialized_default_n`,此时会去检查当前类型是否是平凡类型,这便是resize的第一个限制! + +```cpp +required from '_ForwardIterator std::__uninitialized_default_n(_ForwardIterator, _Size) [with _ForwardIterator = Foo*; _Size = long unsigned int]' + 710 | return __uninitialized_default_n_1<__is_trivial(_ValueType) + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 711 | && __can_fill>:: +``` + +第二个问题:由于我们禁止了默认构造,那么分配器在原地址placement new 一个默认对象的时候失败了,此时便报错,这便是第二个限制! + +```cpp +gcc/14.1.0_1/include/c++/14/bits/stl_construct.h:119:7: error: use of deleted function 'Foo::Foo()' + 119 | ::new((void*)__p) _Tp(std::forward<_Args>(__args)...); + | ^~~~~~~~~~~~~~~~~~~ +``` + +那么说到这里了,便可以进入正文了,可以看到resize的时候会去使用分配器去分配内存,当传递的大小大于size(此时假设在capacity内,即地址有效),那么此时会使用分配器placement new一个默认构造函数的对象。 + +gcc的源码实现可以放在这里给大家看一下,很简单吧?就是调用construct。 + +```cpp +__try +{ + typedef __gnu_cxx::__alloc_traits<_Allocator> __traits; + for (; __n > 0; --__n, (void)++__cur) + __traits::construct(__alloc, std::__addressof(*__cur)); + return __cur; +} +__catch(...) +{ + std::_Destroy(__first, __cur, __alloc); + __throw_exception_again; +} +``` + +好了,resize原理讲明白了,我们来讲讲一开始的问题。 + +## 2.避免调用默认构造 + +如何避免调用默认构造呢? + +那其实很简单了,就是屏蔽调gcc默认的construct,怎么屏蔽了? + +这里便用到了vector的第二个参数:分配器! + +## 3.分配器 + +为了解决这个问题,我们可以使用自定义分配器。自定义分配器可以控制对象的构造行为,允许我们选择在调用 `resize` 时是否使用默认构造。以下是一个简单的自定义分配器实现,只贴了核心代码。 + +```cpp +template +class no_init_alloc { + public: + using value_type = T; + // Override construct method to do nothing + template + void construct(U* p) { + // Do nothing, memory is not initialized + } +}; +``` + +我们只需要在construct不做任何事情即可,此时便可以使用它,即: + +```cpp +std::vector> std_vec; +``` + +此时便可以得到: + +```cpp +[0, 1, 2, 3, 4] +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + +而不是: + +``` +[0, 1, 2, 3, 4] +[0, 1, 2, 3, 4, 0, 0, 0, 0, 0] +``` + +好了,你学会了吗? + +> 本节完 \ No newline at end of file diff --git a/site_extras/cpp-idioms-class-template-over-function-template.md b/site_extras/cpp-idioms-class-template-over-function-template.md new file mode 100644 index 0000000..181b212 --- /dev/null +++ b/site_extras/cpp-idioms-class-template-over-function-template.md @@ -0,0 +1,35 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/class-template-over-function-template/) 的文章,由站点同步脚本导出。 + +# C++ 类模板优于函数模板 + +https://stackoverflow.com/questions/5688355/partial-specialisation-of-member-function-with-non-type-parameter + +最近看到一个很有意思的模版问题,看看大家对函数模版与类模版的基础掌握的如何,对于下面这个例子会出现什么问题? + +你一般使用什么方法进行修复? + +```cpp +template +struct foo { + foo(const T& v) : value_(v) {} + + void bar() { + std::cout << "Generic" << std::endl; + for (int i = 0; i < R; ++i) std::cout << value_ << std::endl; + } + + T value_; +}; + +template <> +void foo::bar() { + std::cout << "Float" << std::endl; + for (int i = 0; i < 3; ++i) std::cout << value_ << std::endl; +} + +template +void foo::bar() { + std::cout << "Double" << std::endl; + for (int i = 0; i < R; ++i) std::cout << value_ << std::endl; +} +``` \ No newline at end of file diff --git a/site_extras/cpp-idioms-dependency-injection-cpp.md b/site_extras/cpp-idioms-dependency-injection-cpp.md new file mode 100644 index 0000000..f1b5217 --- /dev/null +++ b/site_extras/cpp-idioms-dependency-injection-cpp.md @@ -0,0 +1,97 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/dependency-injection-cpp/) 的文章,由站点同步脚本导出。 + +# C++ 依赖注入 + +最近星球里面有个小伙伴让更新一下依赖注入,于是写出了这篇文章,来从实际的例子讲解,本文会讲解一些原理与实现,完整的实现代码懒人版放在星球中,我们开始正文。 + +大纲: + +- 直接依赖 +- 接口依赖注入 +- 模版依赖注入 + +依赖注入是一种常用的设计模式,通常会采用接口/模版的方式提高代码的可测性与维护性,下面我们来举个例子。 + +假设有一个用户服务类UserService,需要通过HTTP客户端与远程服务器进行通信,在这种情况下我们可能会写出如下三种实现。 + +## 1.直接依赖 + +UserService内部直接依赖HttpClient,例如: + +```cpp +class HttpClient { + public: + std::string Get(const std::string& url); + void Post(const std::string& url, const std::string& data); +}; + +class UserService { + private: + HttpClient m_httpClient; +}; +``` + +UserService内部通常会有一些方法去操作m_httpClient,这种方式简单粗暴,但是UserService与具体的HttpClient相耦合,导致难以替换依赖对象,也难以进行单元测试。 + +于是引出了后面两个方法。 + +## 2.接口依赖注入 + +我们的目标是期望测试的时候很方便的测试,而不需要创建一个具体的类,可以由用户mock出一个类去测试,那么我们可以想到接口,将HttpClient抽出来,得到: + +```cpp +struct IHttpClient { + virtual std::string Get(const std::string& url) = 0; + virtual void Post(const std::string& url, const std::string& data) = 0; + virtual ~IHttpClient() = default; +}; +class UserService { + private: + std::unique_ptr m_httpClient; +}; + +``` + +那么在测试的时候便可以mock出一个client进行测试,例如: + +```cc +class MockHttpClient : public IHttpClient { +public: + MOCK_METHOD(std::string, Get, (const std::string& url), (override)); + MOCK_METHOD(void, Post, (const std::string& url, const std::string& data), (override)); +}; + +auto mockHttpClient = std::make_unique(); +UserService userService(std::move(mockHttpClient)); +``` + +经过以上改造,我们可以做到: + +- 在 .cc 文件中隐藏实现细节 +- 针对抽象接口工作 +- 在对象构造期间注入依赖项 + +## 3.模版依赖注入 + +模板依赖注入通过C++模板技术,将依赖对象作为模板参数传递给`UserService`。在实例化模板时,指定具体的HTTP客户端实现。这种方法在编译时解决依赖问题,无需虚函数调用,提高了性能。同时,也使得`UserService`类可以灵活地与不同的HTTP客户端实现进行集成,并且易于进行单元测试。 + +```cpp +template +class UserService { + private: + THttpClient m_httpClient; +}; +``` + +在这种情况下比较好测试,例如: + +```cpp +UserService userService; +``` + +这种方式有如下好处: + +- 在模板实例化期间注入依赖项 +- 无需虚拟调用(注意 THttpClient 不是接口,因此可以在编译时解析调用) + +以上便是本节要讲的所有内容,本节完整代码放于星球,欢迎订阅下载! \ No newline at end of file diff --git a/site_extras/cpp-idioms-header-only-vs-implementation.md b/site_extras/cpp-idioms-header-only-vs-implementation.md new file mode 100644 index 0000000..deb2df1 --- /dev/null +++ b/site_extras/cpp-idioms-header-only-vs-implementation.md @@ -0,0 +1,11 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/header-only-vs-implementation/) 的文章,由站点同步脚本导出。 + +# C++ Header-Only 与实现分离 + +最近看到一些小伙伴问到了几个比较有趣的问题,这里总结一下,这些都是实际面试中出现过的问题,看看你知道多少,考察一下底子。 + +**面试问题1:你通常编写代码的风格是什么?Header-Only?还是声明与实现分离?** + +**面试问题2:C++里面是否所有的函数可以分离实现与声明?为什么?** + +**面试问题3:模版的声明与实现分离有哪些方法?请举例说明各自优缺点,适用场景?** \ No newline at end of file diff --git a/site_extras/cpp-idioms-lazy-evaluation-cpp.md b/site_extras/cpp-idioms-lazy-evaluation-cpp.md new file mode 100644 index 0000000..e9edda3 --- /dev/null +++ b/site_extras/cpp-idioms-lazy-evaluation-cpp.md @@ -0,0 +1,75 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/lazy-evaluation-cpp/) 的文章,由站点同步脚本导出。 + +# C++ 延迟计算惯用法 + +--- + +在软件开发中,延迟计算(**lazy evaluation or call-by-need**)是一种强大的技术,它允许我们推迟表达式的计算,直到真正需要的时候才执行。 + +这种技术在处理大规模数据集或需要动态计算的情况下特别有用。 + +### 1.什么是延迟计算? + +延迟计算是指推迟表达式的计算,直到它们真正需要的时候才执行。这种技术的优点在于可以提高程序的效率和性能,尤其是在处理大规模数据集或需要动态计算的情况下。 + +延迟计算的实现通常通过延迟函数调用或延迟表达式求值来实现。这意味着表达式的计算被推迟到它们被使用的时候,而不是在定义它们的时候就立即执行。 + +### 2.几种实现方法 + +在 C++ 中,我们有多种方法来实现延迟计算。下面是一些常见的方法: + +1. **Lambda 表达式**:Lambda 表达式可以用来表示一个匿名函数,可以在需要的地方进行延迟计算。 + +```cpp +auto delayedCalculation = []() { + std::cout << "延迟计算" << std::endl; + return 42; +}; +``` + +2. **std::function**:`std::function` 可以用于封装函数,使得函数的执行可以延迟到后面的某个时刻。 + +```cpp +std::function delayedCalculation = []() { + std::cout << "延迟计算" << std::endl; + return 42; +}; +``` + +3. **std::bind**:`std::bind` 可以用于绑定函数和参数,并生成一个可调用对象,延迟执行函数调用。 + +```cpp +auto delayedCall = std::bind(delayedCalculation, 42); +``` + +4. **函数指针**:通过指向函数的指针,可以在需要的时候调用相应的函数,实现延迟计算。 + +```cpp +void delayedCalculation() { + std::cout << "延迟计算" << std::endl; +} + +void (*funcPtr)() = &delayedCalculation; +(*funcPtr)(); +``` + +5. **惰性求值**:惰性求值是一种延迟计算的范式,它延迟表达式的求值直到它真正需要的时候。 + +```cpp +auto it = std::begin(vec); +std::cout << "第一个元素:" << *it << std::endl; +``` + +6. **生成器**:生成器是一种用于延迟生成序列的技术,允许按需生成序列中的元素,而不是一次性生成所有元素。 + +```cpp +std::vector generateSequence(int n) { + std::vector result; + for (int i = 0; i < n; ++i) { + result.push_back(i); + } + return result; +} +``` + +除此之外,还有哪些实现呢?欢迎与我一起探讨。 \ No newline at end of file diff --git a/site_extras/cpp-idioms-lookup-table-idiom.md b/site_extras/cpp-idioms-lookup-table-idiom.md new file mode 100644 index 0000000..b17f18e --- /dev/null +++ b/site_extras/cpp-idioms-lookup-table-idiom.md @@ -0,0 +1,134 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/lookup-table-idiom/) 的文章,由站点同步脚本导出。 + +# C++ 查表法惯用法 + +```cpp +#include +#include +#include + + +const int HLL_COUNT = 16; + + +static float harmonic_mean_table(const uint64_t* data) { + static const float harmonic_tables[16] = { + 1.0f / static_cast(1L << 0), + 1.0f / static_cast(1L << 1), + 1.0f / static_cast(1L << 2), + 1.0f / static_cast(1L << 3), + 1.0f / static_cast(1L << 4), + 1.0f / static_cast(1L << 5), + 1.0f / static_cast(1L << 6), + 1.0f / static_cast(1L << 7), + 1.0f / static_cast(1L << 8), + 1.0f / static_cast(1L << 9), + 1.0f / static_cast(1L << 10), + 1.0f / static_cast(1L << 11), + 1.0f / static_cast(1L << 12), + 1.0f / static_cast(1L << 13), + 1.0f / static_cast(1L << 14), + 1.0f / static_cast(1L << 15), + }; + + float harmonic_mean = 0.0f; + for (int i = 0; i < HLL_COUNT; ++i) { + harmonic_mean += harmonic_tables[data[i]]; + } + + return harmonic_mean; +} + +static float harmonic_mean_calc(const uint64_t* data) { + float harmonic_mean = 0.0f; + for (int i = 0; i < HLL_COUNT; ++i) { + harmonic_mean += powf(2.0f, -data[i]); + } + + return harmonic_mean; +} + + +uint64_t global_data[HLL_COUNT]; + +static void CustomArguments(benchmark::internal::Benchmark* b) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution dis(0, HLL_COUNT - 1); + + for (int i = 0; i < HLL_COUNT; ++i) { + global_data[i] = dis(gen); + } +} + +static void BM_HarmonicMeanTable(benchmark::State& state) { + for (auto _ : state) { + benchmark::DoNotOptimize(harmonic_mean_table(global_data)); + } +} +BENCHMARK(BM_HarmonicMeanTable)->Apply(CustomArguments); + + +static void BM_HarmonicMeanCalc(benchmark::State& state) { + for (auto _ : state) { + benchmark::DoNotOptimize(harmonic_mean_calc(global_data)); + } +} +BENCHMARK(BM_HarmonicMeanCalc)->Apply(CustomArguments); +``` + +https://quick-bench.com/q/KtYyI4wutSurcnCrEezhRrH3Oks + +> https://github.com/StarRocks/starrocks/pull/16351 + +## std::inplace_merge vs std::merge + +```cpp +#include +#include +#include +#include +#include + + +std::vector generateTestData(int size) { + std::vector vec(size); + + for (int i = 0; i < size / 2; ++i) { + vec[i] = 2 * i + 1; + } + + for (int i = size / 2; i < size; ++i) { + vec[i] = 2 * (i - size / 2) + 2; + } + + return vec; +} + +static void BM_Merge(benchmark::State& state) { + state.PauseTiming(); + std::vector vec = generateTestData(state.range(0) * 2); + std::vector result(state.range(0) * 2); + state.ResumeTiming(); + for (auto _ : state) { + std::merge(vec.begin(), vec.begin() + state.range(0), vec.begin() + state.range(0), vec.end(), result.begin()); + } +} +BENCHMARK(BM_Merge)->Range(8, 8 << 10); + +static void BM_InplaceMerge(benchmark::State& state) { + state.PauseTiming(); + std::vector vec = generateTestData(state.range(0) * 2); + std::vector::iterator middle = vec.begin() + state.range(0); + state.ResumeTiming(); + + for (auto _ : state) { + std::inplace_merge(vec.begin(), middle, vec.end()); + } +} +BENCHMARK(BM_InplaceMerge)->Range(8, 8 << 10); +``` + +https://quick-bench.com/q/u1WQpxF-KxrvW2s4mrnPkC6ZvLw + +> https://github.com/StarRocks/starrocks/pull/14609 \ No newline at end of file diff --git a/site_extras/cpp-idioms-member-pointer-type-info.md b/site_extras/cpp-idioms-member-pointer-type-info.md new file mode 100644 index 0000000..1c0cff1 --- /dev/null +++ b/site_extras/cpp-idioms-member-pointer-type-info.md @@ -0,0 +1,101 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/member-pointer-type-info/) 的文章,由站点同步脚本导出。 + +# C++ 成员指针与类型信息 + +`::*`,估计99%的人都没用过,甚至没见过,可以留言区打出来!本节将会从0讲解语法,并使用一个项目中的实际例子来实战该用法。 + +[TOC] + +## 1.什么是`::*` + +`C::*` 是 C++ 中的**成员指针**语法,表示指向某个类 `C` 成员的指针。这个语法用于指向类的成员变量或成员函数。与普通指针不同,成员指针不是指向某个对象的具体内存地址,而是指向类中成员的**位置**,并且需要与类的对象实例一起使用。 + +### 1.1 语法 + +- `T C::*` 是一个指向类 `C` 中类型为 `T` 的成员变量的指针。 +- `R (C::*)(Args...)` 是一个指向类 `C` 中返回类型为 `R`,参数列表为 `Args...` 的成员函数的指针。 + +### 1.2 成员变量指针 + +成员变量指针指向某个类的成员变量,使用 `T C::*` 这种形式。 + +例如:下面示例中定义了一个成员指针 `ptr`,它指向 `MyClass` 类的 `x` 成员。`obj.*ptr` 通过成员指针访问对象的成员变量。 + +```cpp +struct MyClass { + int x; + double y; +}; + +int MyClass::*ptr = &MyClass::x; + +MyClass obj; +obj.x = 10; +std::cout << "obj.x = " << obj.*ptr << std::endl; +``` + +### 1.3 成员函数指针 + +成员函数指针指向类的成员函数,使用 `R (C::*)(Args...)` 这种形式。 + +示例:通过 `obj.*funcPtr()` 调用成员函数。 + +```cpp +void (MyClass::*funcPtr)() const = &MyClass::print; +MyClass obj; +(obj.*funcPtr)(); +``` + +## 3.有什么用? + +假设你有一个 ORM 框架,需要从成员指针中获取字段类型,生成对应列的类型,可以这样写: + +```cpp +struct User { + std::string username; + int user_id; +}; + +// 自动生成数据库列的定义 +template +void generate_sql_column(T C::*member_ptr, const std::string& column_name) { + if constexpr (std::is_same_v) { + std::cout << column_name << " VARCHAR(255)" << std::endl; + } else if constexpr (std::is_integral_v) { + std::cout << column_name << " INT" << std::endl; + } +} + +int main() { + // 假设我们想自动为 User 类生成 SQL 表结构 + generate_sql_column(&User::username, "username"); + generate_sql_column(&User::user_id, "user_id"); + + return 0; +} +``` + +## 4.实际项目衍生实用工具! + +在实际项目中,我们可以使用`C::*`提取出对象与成员类型,怎么实现呢? + +答案是:模版 + `C::*` + +如下示例,我们便可以通过`value_type_from_member_pointer`与`class_type_from_member_pointer`来提取出对应的成员类型与类类型,非常方便的工具。 + +```cpp +template +struct from_member_pointer_helper { + using value_type = T; + using class_type = C; +}; + +template +struct from_member_pointer : from_member_pointer_helper> {}; + +template +using value_type_from_member_pointer = from_member_pointer::value_type; + +template +using class_type_from_member_pointer = from_member_pointer::class_type; +``` \ No newline at end of file diff --git a/site_extras/cpp-idioms-mixin-idiom.md b/site_extras/cpp-idioms-mixin-idiom.md new file mode 100644 index 0000000..50de53d --- /dev/null +++ b/site_extras/cpp-idioms-mixin-idiom.md @@ -0,0 +1,228 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/mixin-idiom/) 的文章,由站点同步脚本导出。 + +# C++ Mixin 惯用法 + +大家好,我是光城,今天给大家分享C++那些事里面的一个惯用法:mixin + +混合(Mixins)是Lisp中的一个概念。混合是类的一部分,意味着它旨在与其他类或混合组合在一起。 +常规独立类(例如Person)与混合的区别在于,混合模拟了一些小功能片段(例如打印或显示),并不用于独立使用。相反,它应该与需要此功能的其他类(例如Person)组合在一起。 + +因此,混合的目的是允许类似于多重继承的东西。 + +在C++汇总mixin的基本写法如下: + +```cpp +template +class Mixin : public Super { +. /* mixin body */ +}; +``` + +或者 + +```cpp +template +class Mixin : public Super... { + public: + Mixin() : Super...() {} + // ... +}; +``` + +**注:本节的所有示例代码可从星球获取。** + +## 示例1:缩放与旋转 + +假设我们要对一个长方形/正方形进行缩放、旋转、添加边框等,这些操作都会影响其宽度与高度,我们可以使用mixin来实现。 + +首先,我们可以定义好正常形与长方形作为mixin的基类。 + +```cpp +// 正方形类 +class Square : public Shape { +public: + Square(int sideLength) : sideLength(sideLength) {} + int GetWidth() const override { return sideLength; } + int GetHeight() const override { return sideLength; } +private: + int sideLength; +}; + +// 长方形类 +class Rectangle : public Shape { +public: + Rectangle(int width, int height) : width(width), height(height) {} + int GetWidth() const override { return width; } + int GetHeight() const override { return height; } +private: + int width; + int height; +}; +``` + +随后使用mixin子类去拓展功能,例如:一个缩放的mixin我们可以自定义一个类,它的宽度与高度等于scale乘以对应的宽高。 + +```cpp +// 缩放的Mixin +template +class Scale : public Base { + public: + int GetScaledWidth() const { return Base::GetWidth() * ScaleX; } + int GetScaledHeight() const { return Base::GetHeight() * ScaleY; } +}; + +``` + +像这种场景非常适合使用mixin。 + +使用的时候: + +```cpp +using ModifiedShape = Border>, 10>; +``` + +## 示例2:redo与undo + +完整示例模拟一个redo与undo操作,我们便可以在数据之间来回的切换。`Redoable> `,在这里我们可以将这些类混合到一起使用了。 + +```cpp +#include + +using namespace std; + +struct Number { + typedef int value_type; + int n; + void set(int v) { + n = v; + } + int get() const { + return n; + } +}; + +template +struct Undoable : public BASE { + typedef T value_type; + T before; + void set(T v) { + before = BASE::get(); + BASE::set(v); + } + void undo() { + BASE::set(before); + } +}; + +template +struct Redoable : public BASE { + typedef T value_type; + T after; + void set(T v) { + after = v; + BASE::set(v); + } + void redo() { + BASE::set(after); + } +}; + +typedef Redoable> ReUndoableNumber; + +int main() { + ReUndoableNumber mynum; + mynum.set(42); + mynum.set(84); + cout << mynum.get() << '\n'; // Output: 84 + mynum.undo(); + cout << mynum.get() << '\n'; // Output: 42 + mynum.redo(); + cout << mynum.get() << '\n'; // Output: back to 84 +} +``` + +## 示例3:重复打印 + +再举一个拼积木的例子:重复打印。 + +首先,有一个类,我们可以print这个人的名字。 + +```cpp +class Name +{ +public: + Name(std::string firstName, std::string lastName) + : firstName_(std::move(firstName)) + , lastName_(std::move(lastName)) {} + + void print() const + { + std::cout << lastName_ << ", " << firstName_ << '\n'; + } + +private: + std::string firstName_; + std::string lastName_; +}; +``` + +那如果我想重复打印呢? + +我们可以这样玩,使用mixin,将Name类传递进去。 + +```cpp +template +struct RepeatPrint : Printable +{ + explicit RepeatPrint(Printable const& printable) : Printable(printable) {} + void repeat(unsigned int n) const + { + while (n-- > 0) + { + this->print(); + } + } +}; + +template +RepeatPrint repeatPrint(Printable const& printable) +{ + return RepeatPrint(printable); +} +``` + +使用: + +``` +Name ned("Eddard", "Stark"); +repeatPrint(ned).repeat(10); +``` + +此时,便得到重复n次的打印结果。 + +那么mixin比较适合什么场景呢? + +- 保持原来的类不变, +- 客户端代码不直接使用原始类,它需要将其包装到 mixin 中才能使用增强功能, + +## 标准库 + +在标准库当中有一个使用mixin技术:std::nested_exception。 + +std::nested_exception 是一个多态 mixin 类,它可以捕获并存储当前异常,从而可以在彼此之间嵌套任意类型的异常。 + +```cpp +template +struct _Nested_exception : public _Except, public nested_exception +{} +``` + +在源码中我们可以看到使用了nested_exception作为mixin的基类。 + +## 参考 + +> https://cedanet.com.au/ceda/Xc++/$mixin/mixins-in-straight-c++.php +> +> https://www.fluentcpp.com/2017/12/12/mixin-classes-yang-crtp/ +> +> https://stackoverflow.com/questions/18773367/what-are-mixins-as-a-concept \ No newline at end of file diff --git a/site_extras/cpp-idioms-trigraphs-cpp.md b/site_extras/cpp-idioms-trigraphs-cpp.md new file mode 100644 index 0000000..2e14244 --- /dev/null +++ b/site_extras/cpp-idioms-trigraphs-cpp.md @@ -0,0 +1,51 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-idioms/trigraphs-cpp/) 的文章,由站点同步脚本导出。 + +# C++ 三字母词 + +## C++那些事之删除三字母 + +C++17 之前,Trigraph 是用 ISO/IEC 646 等小字符代码编写程序的功能,在 C89 中引入。 ISO/IEC 646 缺少 C 中使用的以下字符,因此`??`请使用以下字符进行转义: + +| 三字母内容 | 替换的内容 | +| ---------- | ---------- | +| `??=` | `#` | +| `??(` | `[` | +| `??)` | `]` | +| `??<` | `{` | +| `??>` | `}` | +| `??/` | `\` | +| `??!` | `|` | +| `??-` | `~` | +| `??'` | `^` | + +`\?`它被转义以防止被三字母表替换。 + +C++17三字母被删除,`??=`等不再被替换。 + +## 示例 + +C++17之前: + +```cpp +std::cout << "??=" << std::endl; +std::cout << "??(" << std::endl; +std::cout << "?\?(" << std::endl; +``` + +输出: + +``` +# +[ +??( +``` + +C++17及之后: + +``` +??= +??( +??( +``` + +现在可能由于Unicode等字符编码已经普遍存在,不再需要三字母组,而gcc等编译器默认禁用三字母组。 \ No newline at end of file diff --git a/site_extras/cpp-memory-debug-address-sanitizer-asan.md b/site_extras/cpp-memory-debug-address-sanitizer-asan.md new file mode 100644 index 0000000..a6aebe9 --- /dev/null +++ b/site_extras/cpp-memory-debug-address-sanitizer-asan.md @@ -0,0 +1,233 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-memory-debug/address-sanitizer-asan/) 的文章,由站点同步脚本导出。 + +# AddressSanitizer 内存检测 + +C和C++是非常不安全且容易出错的编程语言,Address Sanitizer是由Google开发的一种工具,用于检测内存访问错误,如使用后释放(use-after-free)和内存泄漏。它已集成到GCC版本>= 4.8中,可用于C和C++代码。Address Sanitizer使用运行时插桩来跟踪内存分配,这意味着您必须使用AddressSanitizer构建代码,以充分利用其功能。 + +- [Use after free](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleUseAfterFree) (dangling pointer dereference) +- [Heap buffer overflow](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleHeapOutOfBounds) +- [Stack buffer overflow](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleStackOutOfBounds) +- [Global buffer overflow](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleGlobalOutOfBounds) +- [Use after return](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleUseAfterReturn) +- [Use after scope](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleUseAfterScope) +- [Initialization order bugs](https://github.com/google/sanitizers/wiki/AddressSanitizerInitializationOrderFiasco) +- [Memory leaks](https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer) + +本文将会以上面8种场景来介绍内存检测工具**AddressSanitizer**。 + +## 1.Use after free + +以下面代码为例。首先,通过`new`关键字分配了一个整数的内存空间,并将其地址赋给指针 `ptr`。然后,使用 `delete` 关键字释放了这块内存。但是,在释放后,程序仍然尝试通过指针 `ptr` 访问已经释放的内存,并将值 10 赋给该内存位置,这就是使用后释放错误。 + +```cpp +#include + +int main() { + int* ptr = new int; + delete ptr; + *ptr = 10; // Use after free + return 0; +} +``` + +在编译时添加下面选项。 + +``` +-fsanitize=address +``` + +运行生成的bin文件,得到如下结果。 + +![](../assets/images/2024/mem/address/heap_use_after_free.png) + +## 2.heap buffer overflow + +首先,通过 `new` 关键字在堆上分配了一个包含两个整数的数组,并将数组的起始地址赋给指针 `arr`。然后,程序试图将值 10 存储在数组的第三个位置(索引为 2),这超出了数组的有效范围,导致堆缓冲区溢出错误。 + +``` +#include + +int main() { + int* arr = new int[2]; + arr[2] = 10; // Heap buffer overflow + delete[] arr; + return 0; +} + +``` + +同上面用法: + +``` +g++-13 heap_buffer_overflow.cc -Wall -g -fsanitize=address +``` + +然后: + +``` +./a.out +``` + +得到下面检测结果。 + +![heap_buffer_overflow](../assets/images/2024/mem/address/heap_buffer_overflow.png) + +## 3.stack buffer overflow + +在 `vulnerableFunction` 函数中,定义了一个包含 8 个字符的字符数组 `buffer`。然后,使用 `std::cin` 从标准输入读取数据存储到 `buffer` 中,但没有限制输入的大小。如果输入的字符数超过了 `buffer` 的大小,就会发生栈缓冲区溢出错误。 + +```cpp +void vulnerableFunction() { + char buffer[8]; + std::cin >> buffer; // Stack buffer overflow + std::cout << "You entered: " << buffer << std::endl; +} +``` + +用法同上。 + +![stack_buffer_overflow](../assets/images/2024/mem/address/stack_buffer_overflow.png) + +## 4.global buffer overflow + +全局缓冲区(`buffer` 数组)在程序的全局范围内定义,而没有受到足够的输入大小限制。如果输入的字符数超过了缓冲区的大小,就会发生全局缓冲区溢出错误。与栈缓冲区溢出类似,全局缓冲区溢出可能导致程序的不稳定性和安全性问题。 + +```cpp +#include + +char buffer[8]; + +int main() { + std::cin >> buffer; // 全局缓冲区溢出错误 + std::cout << "You entered: " << buffer << std::endl; + return 0; +} +``` + +用法同上,得到: + +![global_buffer_overflow](../assets/images/2024/mem/address/global_buffer_overflow.png) + +## 5.use after return + +在函数 `f` 中,局部变量 `i` 的地址被返回,但在 `main` 函数中,尝试通过返回的指针访问已经被销毁的局部变量。当出现这种场景是,可以对bin文件进行检测,设置`ASAN_OPTIONS=detect_stack_use_after_return=1`。 + +``` +g++-13 stack_use_after_return.cc -Wall -g -fsanitize=address +ASAN_OPTIONS=detect_stack_use_after_return=1 ./a.out +``` + +代码: + +```cpp +#include + +int *f() { + int i = 42; + int *p = &i; + return p; +} + +int main() { return *f(); } + +``` + +我们便会得到: + +![stack_use_after_return](../assets/images/2024/mem/address/stack_use_after_return.png) + +## 6.use after scope + +将局部变量 `x` 的地址赋给指针 `p`,然而,在超出该内部作用域后,尝试通过指针 `p` 访问已经超出范围的局部变量 `x`,导致范围外使用错误。这也是未定义行为。 + +```cpp +#include +int main() { + int* p = 0; + { + int x = 0; + p = &x; + } + *p = 5; + return 0; +} +``` + +编译与运行: + +``` +g++-13 stack_use_after_scope.cc -Wall -g -fsanitize=address +ASAN_OPTIONS=detect_stack_use_after_scope=0 ./a.out +``` + +实测clang++可以,g++无法检测。 + +![stack_use_after_scope](../assets/images/2024/mem/address/stack_use_after_scope.png) + +## 7.Initialization order bugs + +在两个不同的源文件中,存在全局变量 `a` 的初始化,但初始化的顺序不确定。在 `init_order_b.cc` 文件中,尝试使用变量 `a` 的值,但由于初始化顺序不一致,可能导致未定义的行为。 + +代码如下: + +```cpp +// init_order_a.cc +int duplicate(int n) { return n * 2; } +auto a = duplicate(2); + +// init_order_b.cc +#include + +extern int a; +auto b = a; + +int main() { + std::cout << b << std::endl; + return 0; +} +``` + +当我们编写这样的代码时,其结果是不确定的,可能是4也可能是0,取决于初始化的顺序。 + +例如: + +``` +clang++ init_order_a.cc init_order_b.cc -Wall -g -fsanitize=address +``` + +这样输出是4,而先编译b,后编译a,得到0。 + +``` +clang++ init_order_b.cc init_order_a.cc -Wall -g -fsanitize=address +``` + +当出现这种问题时,我们可以使用AddressSanitizer检测初始化顺序。 + +```cpp +ASAN_OPTIONS=check_initialization_order=true ./a.out +``` + +![init_order](../assets/images/2024/mem/address/init_order.png) + +## 8.memory leak + +通过 `new` 关键字动态分配了一个整数的内存,但在程序结束前未使用 `delete` 关键字释放这块内存,导致内存泄漏。 + +```cpp +#include + +int main() { + int* ptr = new int; + // Missing delete ptr; // Memory leak + return 0; +} + +``` + +设置`ASAN_OPTIONS=detect_leaks=1`。 + +``` +ASAN_OPTIONS=detect_leaks=1 ./a.out +``` + +![memory_leak](../assets/images/2024/mem/address/memory_leak.png) \ No newline at end of file diff --git a/site_extras/cpp-memory-debug-cpp-memory-leak-detection.md b/site_extras/cpp-memory-debug-cpp-memory-leak-detection.md new file mode 100644 index 0000000..3c18748 --- /dev/null +++ b/site_extras/cpp-memory-debug-cpp-memory-leak-detection.md @@ -0,0 +1,133 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-memory-debug/cpp-memory-leak-detection/) 的文章,由站点同步脚本导出。 + +# C++ 内存泄漏检测 + +## C++那些事之如何检测内存 + +我们在编写C/C++代码时,内存问题无处不在,无论是工作还是面试,都离不开内存的话题。 + +今天使用一个例子来Step by Step的上手valgrind检测C++内存。 + +模拟三个问题: + +- 释放内存问题检测 +- 写溢出检测 +- 读溢出检测 + +你将会学习到: + +- 如果检测这三个问题 +- 如果使用gdb + valgrind一起调试 + +欢迎一起讨论。 + +## 1.示例引入 + +大家看一下下面这个代码,我们会发现有很多问题: + +- strcpy不安全,究竟多大的内存大小,可能会越界 +- print时msg访问越界 +- 析构函数没有释放内存 + +是不是像极了自己曾经写的代码,哈哈。 + +本节就来检测出这些问题。 + +```cpp +class Message { +private: + char *msg; + +public: + Message(const char *text) { + msg = new char[3]; + std::strcpy(msg, text); + } + + void printOutOfRange() { + std::cout << msg[5] << std::endl; + } + + ~Message() { + + } +}; + +int main() { + Message message("hello"); + message.printOutOfRange(); + + return 0; +} +``` + +## 2.valgrind + +valgrind安装: + +``` +brew install valgrind // mac +sudo apt-get install install // ubuntu +``` + +更多系统的安装参考官方地址: + +> https://valgrind.org/downloads/ + +## 3.使用 + +首先编译时,建议使用debug,-g,这样的话可以检测到代码哪一行有问题。 + +``` +g++ val_test.cc -g +``` + +然后我们执行: + +``` +valgrind --leak-check=full ./a.out +``` + +我们便得到了valgrind给我们的信息: + +![](../assets/images/2024/mem/img/detect.png) + +此时,在上面我们可以看到几个问题: + +- Invalid write + +发生在val_test.cc::11 + +- Invalid read + +发生在val_test.cc:10 + +- 内存泄漏 + +1 allocs, 0 frees,3 bytes allocated. + +这些信息可以快速让我们定位到哪一行代码有问题。 + +## 4.gdb调试 + +当我们给它加上`--vgdb-error=0`后,我们便可以很方便的调试代码了,一边调试,一边看哪里有问题。 + +```cpp +valgrind --leak-check=full --vgdb-error=0 ./a.out +``` + +![](../assets/images/2024/mem/img/attach.png) + +随后,我们gdb上去 + +```cpp +gdb -q a.out +``` + +把上面的target那一行粘贴到 gdb里面,然后continue,我们便可以看到对应行的代码以及valgrind会继续输出的信息。 + +![](../assets/images/2024/mem/img/gdb.png) + +![](../assets/images/2024/mem/img/continue.png) + +本节带大家实战内存问题,如果对你有帮助欢迎转发。 \ No newline at end of file diff --git a/site_extras/cpp-memory-debug-valgrind-massif-memory.md b/site_extras/cpp-memory-debug-valgrind-massif-memory.md new file mode 100644 index 0000000..e54134c --- /dev/null +++ b/site_extras/cpp-memory-debug-valgrind-massif-memory.md @@ -0,0 +1,19 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-memory-debug/valgrind-massif-memory/) 的文章,由站点同步脚本导出。 + +# Valgrind Massif 内存分析 + +gpstart -a --wrapper=valgrind --wrapper-args=" --tool=massif --time-unit=B --detailed-freq=1 --massif-out-file=/tmp/massif.out.%p" + +![](../assets/images/2024/mem/massif/mss.png) + +``` +ms_print xxx.out | more +``` + +https://valgrind.org/docs/manual/ms-manual.html + +![](../assets/images/2024/mem/massif/ms_print.png) + + + +valgrind -v --tool=massif --time-unit=B --detailed-freq=1 --massif-out-file=./massif.out ./a.out \ No newline at end of file diff --git a/site_extras/cpp-projects-cpp-fnonce-implementation.md b/site_extras/cpp-projects-cpp-fnonce-implementation.md new file mode 100644 index 0000000..c8dccd4 --- /dev/null +++ b/site_extras/cpp-projects-cpp-fnonce-implementation.md @@ -0,0 +1,7 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-projects/cpp-fnonce-implementation/) 的文章,由站点同步脚本导出。 + +# 手撕 C++ FnOnce + +## 《C++那些事》手撕C++版本的Fn、FnOnce、FnMut + +## 1.目标 \ No newline at end of file diff --git a/site_extras/cpp-projects-cpp-grpc-demo.md b/site_extras/cpp-projects-cpp-grpc-demo.md new file mode 100644 index 0000000..3cce26c --- /dev/null +++ b/site_extras/cpp-projects-cpp-grpc-demo.md @@ -0,0 +1,182 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-projects/cpp-grpc-demo/) 的文章,由站点同步脚本导出。 + +# C++ gRPC 小项目 + +## C++那些事之grpc小Demo + +github上比较火的rpc有grpc、brpc,腾讯内部比较牛逼的trpc等等,这些rpc支持不同的语言、不同平台。今天来聊聊如何使用grpc,从一个简单的demo入手,整个项目使用CMake构建,一个非常标准的rpc项目管理,所有代码已更新至星球,获取方式见最后的二维码。 + +## 1.Step by Step 环境 + +如果你的电脑是mac,不好意思,暂时用不了最新版本的grpc,最新版本的grpc用了abseil,编译会有问题,官方暂时未解决,如果大家知道如何解决,可以留言,我这边目前未找到解决方法。 + +所以,建议安装低版本的grpc在你的mac上,或者在linux直接安装,linux上可以使用最新版本grpc,一次安装没有任何问题。 + +安装方式: + +``` +git clone -b v1.57.0 git@github.com:grpc/grpc.git +``` + +然后初始化submodule + +```cpp +git submodule update --init +``` + +cmake编译安装: + +```cpp +cd cmake +mkdir build +cd build +cmake -DgRPC_INSTALL=ON -DgRPC_BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX=/usr/local/grpc ../.. + +make -j8 +make -j8 install // root 权限 +``` + +大家可以修改上面的安装目录,注意install时需要root权限。 + +安装完之后,将你的bin目录配置到path,例如: + +``` +export PATH=/usr/local/grpc/bin:$PATH +``` + +## 2.反转字符串Demo + +环境搞定了之后,我们便可以上手搞事了。 + +### 2.1 协议 + +第一件事是确定rpc协议: + +```cpp +syntax = "proto3"; + +package stringreverse; + +service StringReverse { + rpc sendRequest (StringRequest) returns (StringReply) {} +} + +message StringRequest { + string original = 1; +} + +message StringReply { + string reversed = 1; +} +``` + +非常简单的协议,请求一个字符串,返回一个字符串。 + +### 2.2 客户端 + +构造rpc请求字符串,然后调用sendRequest接口发送出去即可。 + +```cpp +std::string str = "hello world grpc!"; +std::cout << "Original string: " << str << std::endl; +StringRequest request; +StringReply reply; +ClientContext context; +request.set_original(str); +Status status = stub->sendRequest(&context, request, &reply); +if (status.ok()) { + std::cout << "Reversed string: " << reply.reversed() << std::endl; +} +// do something +``` + +### 2.3 服务端 + +通过继承Service实现反转字符串逻辑。 + +```cpp +class ReverseServiceImplementation final : public StringReverse::Service { + Status sendRequest(ServerContext* context, const StringRequest* request, + StringReply* reply) override { + // 反转逻辑 + } +} +``` + +最后注册service即可: + +```cpp +builder.RegisterService(&service); +std::unique_ptr server(builder.BuildAndStart()); +std::cout << "Server listening on port: " << server_address << std::endl; +``` + +## 3.运行 + +创建编译目录: + +``` +mkdir build +cmake .. +# 输出 +-- The C compiler identification is GNU 10.2.1 +-- The CXX compiler identification is GNU 10.2.1 +-- Detecting C compiler ABI info +-- Detecting C compiler ABI info - done +-- Check for working C compiler: /opt/rh/devtoolset-10/root/usr/bin/cc - skipped +-- Detecting C compile features +-- Detecting C compile features - done +-- Detecting CXX compiler ABI info +-- Detecting CXX compiler ABI info - done +-- Check for working CXX compiler: /opt/rh/devtoolset-10/root/usr/bin/c++ - skipped +-- Detecting CXX compile features +-- Detecting CXX compile features - done +-- Looking for pthread.h +-- Looking for pthread.h - found +-- Performing Test CMAKE_HAVE_LIBC_PTHREAD +-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Failed +-- Looking for pthread_create in pthreads +-- Looking for pthread_create in pthreads - not found +-- Looking for pthread_create in pthread +-- Looking for pthread_create in pthread - found +-- Found Threads: TRUE +-- Found Protobuf: /usr/local/grpc/bin/protoc-23.4.0 (found version "23.4.0") +-- Using protobuf +-- Using gRPC 1.57.0 +-- Configuring done +-- Generating done +-- Build files have been written to: /code/stringreverse/build +``` + +make编译: + +``` +[ 10%] Generating stringreverse.pb.cc, stringreverse.pb.h, stringreverse.grpc.pb.cc, stringreverse.grpc.pb.h +Scanning dependencies of target server +[ 20%] Building CXX object CMakeFiles/server.dir/server.cc.o +[ 30%] Building CXX object CMakeFiles/server.dir/stringreverse.pb.cc.o +[ 40%] Building CXX object CMakeFiles/server.dir/stringreverse.grpc.pb.cc.o +[ 50%] Linking CXX executable server +[ 50%] Built target server +Scanning dependencies of target client +[ 60%] Building CXX object CMakeFiles/client.dir/client.cc.o +[ 70%] Building CXX object CMakeFiles/client.dir/stringreverse.pb.cc.o +[ 80%] Building CXX object CMakeFiles/client.dir/stringreverse.grpc.pb.cc.o +[ 90%] Linking CXX executable client +[100%] Built target client +``` + +运行server: + +``` +[light@i-i339drlk build]$ ./server +Server listening on port: 0.0.0.0:5000 +``` + +运行client: + +```cpp +[light@i-i339drlk build]$ ./client +Original string: hello world grpc! +Reversed string: !cprg dlrow olleh +``` \ No newline at end of file diff --git a/site_extras/cpp-projects-cpp-ipc-practice.md b/site_extras/cpp-projects-cpp-ipc-practice.md new file mode 100644 index 0000000..62b413c --- /dev/null +++ b/site_extras/cpp-projects-cpp-ipc-practice.md @@ -0,0 +1,83 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-projects/cpp-ipc-practice/) 的文章,由站点同步脚本导出。 + +# C++ 进程间通信实战 + +进程间通信是一个非常重要的话题,特别是像一些大型项目都有它的影子,例如:PostgreSQL使用了管道完成copy的进程间通信,那么本节也将基于这个主题,使用C++去搭建一个进程间通过管道通信的demo出来,本节将会结合前面几节讲的内容,给出一个比较完整的实战例子出来: + +- mutex +- condition_variable +- thread + +注:本节的完整代码会放在星球中。 + +首先一个很简单的管道用法为: + +- 创建一个管道 + +它会creating a pipe, forking, and invoking the shell. + +```cpp +// FILE *popen(const char *command, const char *type); +pipe = popen(command.c_str(), "w"); +``` + +- 关闭一个管道 + +```cpp +// int pclose(FILE *stream); +pclose(pipe); +``` + +接下来我们就只需要往里面写与读了,原理也很简单,例如: + +此时便会把data写入到前面创建的管道中。 + +```cpp +// size_t fwrite( const void* buffer, size_t size, size_t count, FILE* stream ); +size_t written = fwrite(data.c_str(), sizeof(char), data.size(), pipe); +``` + +如果从管道中读取,我们可以使用fread或者fgets,`fgets` 会在读取到换行符或达到缓冲区大小时停止,而 `fread` 会尽可能多地读取数据,直到达到指定的字节数或遇到文件结尾。同理,上面的fwrite可以替换为fputs。 + +现在,一个管道的基本使用介绍完了,如何写一个完整的demo? + +很简单,我们只需要创建两个线程,一个读,一个写,然后写线程往pipe里面写数据,读线程从pipe里面读即可。 + +例如: + +```cpp +std::thread writer_thread(write_to_pipe); +std::thread reader_thread(read_from_pipe); + +writer_thread.join(); +reader_thread.join(); +``` + +写线程需要做的事情就是调用fwrite丢数据即可 + +```cpp +ProcessWriter writer("cat > /tmp/pipe"); + +// 写入数据 +std::string message = "Hello pipe!\n"; +if (!writer.write(message)) { + std::cerr << "Failed to write data to pipe" << std::endl; +} +``` + +同理,读线程取数据: + +```cpp +ProcessReader reader("/tmp/pipe"); +// 读取数据 +std::string message; +while (reader.read(message)) { + // do something +} +``` + +最后的效果就是: + +```cpp +Read from pipe: Hello pipe! +``` \ No newline at end of file diff --git a/site_extras/cpp-projects-cpp-short-url-demo.md b/site_extras/cpp-projects-cpp-short-url-demo.md new file mode 100644 index 0000000..8b2ccad --- /dev/null +++ b/site_extras/cpp-projects-cpp-short-url-demo.md @@ -0,0 +1,70 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-projects/cpp-short-url-demo/) 的文章,由站点同步脚本导出。 + +# C++ 短链接服务 Demo + +真实的短链接相对来说比较复杂,例如:hash算法、放号系统等等。 + +今天只是从小项目角度模拟一个短链接实现。 + +如何通过短短的几十行代码快速实现一个?其中涉及的几个关键点: + +- 如何将长链接缩短 +- 如何存储映射关系 + +映射关系就不用说了,整一个map持久化内存,如果涉及到数据库,需要通过数据库的jdbc/odbc等接口访问写入库当中。 + +长链接缩短这里采用随机算法+固定长度思路来实现。 + +## 1.随机 + +通过uniform_int_distribution,生成一个`[a, b]`区间的随机数,这里随机从下列字符串中每次取一个字符,最后拼接起来就是短链接。 + +```cpp +string characters{ + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}; +``` + +包含: + +- 数字 +- 小写字母 +- 大写字母 + +```cpp +uniform_int_distribution distribution(0, characters.size() - 1); +string code; +for (int i = 0; i < length; ++i) + code += characters[distribution(generator)]; +``` + +## 2.存储 + +每次将其存储到map中即可,例如: + +```cpp +if (links.count(originalLink)) { + return links[originalLink]; +} + +shortCode = generateRandomCode(len); +string shortenedLink = base_url + shortCode; +links[shortenedLink] = originalLink; +``` + +这里保留了基本url,例如: + +```cpp +string baseUrl = "https://www.google.com.hk/search"; +LinkShortener shortener(baseUrl); + +string originalLink = "https://www.google.com.hk/search?q=asdsad&oq=asdsad&aqs=chrome.0.69i59j69i57j69i59l2.634j0j4&sourceid=chrome&ie=UTF-8"; +string shortenedLink = shortener.shortenLink(originalLink, 8); +``` + +最后便可以将这个url缩短为: + +``` +https://www.google.com.hk/searchtXuOlOSt +``` + +是不是很简单,本节的测试与完整代码已在星球更新,欢迎加入一起学习成长~ \ No newline at end of file diff --git a/site_extras/cpp-projects-cpp17-sqlite-folder-size.md b/site_extras/cpp-projects-cpp17-sqlite-folder-size.md new file mode 100644 index 0000000..c1c2327 --- /dev/null +++ b/site_extras/cpp-projects-cpp17-sqlite-folder-size.md @@ -0,0 +1,134 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-projects/cpp17-sqlite-folder-size/) 的文章,由站点同步脚本导出。 + +# C++17 与 SQLite 统计目录大小 + +## 项目实战:使用 C++17 和 SQLite 实现文件夹大小计算与存储 + +[TOC] + +本节将会围绕一个项目背景展开:假设业务中需要定时检查某些文件夹大小,并做一下持久化操作,C++如何去完成这一个项目呢? + +本节带你通过一个小项目,使用 C++17 和 SQLite 实现一个文件夹大小计算与存储的系统,帮助你更好地管理你的文件系统。 + +### 项目目标 + +我们将实现以下功能: +1. **递归计算指定目录及其子目录的大小**。 +2. **将计算结果保存到 SQLite 数据库**,方便后续查询和分析。 +3. **可扩展性强**:你可以轻松修改项目,添加多线程支持、优化性能,或将结果存储到其他数据库中。 + +**注:懒人版,完整代码已更新星球。** + +### 项目实现步骤 + +#### 1. 环境 + +首先,你需要确保开发环境中已经安装了以下工具: +- **支持C++17 编译器**:如 g++,通过以下命令可以检查版本: + + ```bash + g++ --version + ``` + 确保 g++ 支持 C++17 标准。 + +- **SQLite3**:数据库操作我们会使用 SQLite,你可以通过包管理器来安装它: + ```bash + sudo apt-get install sqlite3 libsqlite3-dev + ``` + 安装完后,SQLite 数据库的开发库 `libsqlite3-dev` 可以确保我们在 C++ 中能正常使用 SQLite。 + +#### 2. 创建项目结构 + +我们将把代码分为多个模块,保持清晰的架构: +- `folder_size.h` / `folder_size.cpp`:负责递归计算目录及其子目录大小。 +- `database.h` / `database.cpp`:封装 SQLite 数据库操作逻辑。 +- `main.cpp`:主程序入口,负责整体流程的控制。 + +项目的目录结构如下: +``` +project/ +│── folder_size.h +│── folder_size.cpp +│── database.h +│── database.cpp +└── main.cpp +``` + +#### 3. 代码实现 + +接下来,我们将一步步实现每个模块。 + +##### 3.1 目录计算模块 + +C++17 提供了`std::filesystem` 库来递归遍历目录并计算大小。因此其核心实现如下: + +```cpp +uintmax_t total_size = 0; +for (const auto& entry : fs::recursive_directory_iterator(dir_path)) { + if (fs::is_regular_file(entry.path())) { + total_size += fs::file_size(entry.path()); + } +} +``` + +如果要计算根目录 再计算一次即可。 + +##### 3.2 SQLite数据库操作 + +引入: + +``` +#include +``` + +使用: + +```cpp +sqlite3_close(db_); // 关闭 + +// 插入 +for (const auto& [folder, size] : folder_sizes) { + const std::string insert_sql = + "INSERT OR REPLACE INTO FolderSizes (Folder, Size) " + "VALUES ('" + folder + "', " + std::to_string(size) + ");"; + if (!ExecuteSQL(insert_sql)) { + return false; + } +} + +// 执行 +sqlite3_exec +``` + +#### 4. 编译与运行 + +编译项目非常简单: + +```bash +mkdir build +cmake ../ +make -j8 +``` + +然后,运行程序,计算目录大小并存储到数据库: + +```bash +./folder_size_calculator +``` + +程序将会递归计算指定目录的大小,并将结果保存到 `folder_sizes.db` 数据库中。 + +#### 5. 查询数据库 + +最后,你可以通过 + + SQLite 命令行工具查询数据库中的内容: + +```bash +sqlite3 folder_sizes.db +sqlite> SELECT * FROM FolderSizes; +``` + +这将显示所有已保存的目录及其对应的大小。 + +> 本节完 \ No newline at end of file diff --git a/site_extras/cpp-projects-google-benchmark-cpp.md b/site_extras/cpp-projects-google-benchmark-cpp.md new file mode 100644 index 0000000..24cd502 --- /dev/null +++ b/site_extras/cpp-projects-google-benchmark-cpp.md @@ -0,0 +1,109 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-projects/google-benchmark-cpp/) 的文章,由站点同步脚本导出。 + +# Google Benchmark 性能测试 + +## 《C++那些事》之开启你的BenchMark测试 + +## 0.导语 + +本节目标:完成一个BenchMark小项目! + +在平时开发中,如何测试自己的接口性能呢?C++里面如何快速搭建一个BenchMark测试框架呢? + +本节将Step By Step开启BenchMark入门的第一课。 + +## 1.项目结构 + +我们以vector与list插入为例,来讲解BenchMark项目。 + +这次项目是基于Bazel来管理的,如果还不知道如何配置相关环境的,请参考之前的视频课程/文档资料。 + +创建一个项目结构为: + +``` +``` + +BenchMark采用google的开源项目: + +```cpp +https://github.com/google/benchmark.git +``` + +像以前大家使用第三方库,需要自己源码编译一波,生成so库,然后链接,太麻烦了!这里直接使用bazel管理,只需要在WORKSPACE里面,添加依赖的git项目,例如这里采用google benchmark的v1.7.1版本。 + +```cpp +git_repository( + name = "google_benchmark", + remote = "https://github.com/google/benchmark.git", + tag = "v1.7.1", +) +``` + +随后,我们需要实现主逻辑,创建一个src目录,里面添加一个BUILD文件:目的在于编译过会cpp文件,注意:这里依赖了benchmark库,使用方法为@**前面配置的name**//:benchmark + +```cpp +cc_binary( + name = "vector_list", + srcs = ["vector_list.cc"], + deps = [ + "@google_benchmark//:benchmark", + ] +) +``` + +这样的话,在cc文件中便可以使用了: + +```cpp +#include +``` + +### 2.Demo环节 + +编写两个插入函数,非常简单,框架如下所示,里面有一些需要说明的,如下: + +- state为benchmark的运行时状态信息 + - 例如:休眠控制、入参数控制等 +- state.range(0)便是获取第0个参数 +- DoNotOptimize表示不做编译器优化 + +```cpp +// 测试 std::vector 的插入速度 +static void BM_vectorInsert(benchmark::State& state) { + for (auto _ : state) { + std::vector v; + for (size_t i = 0; i < state.range(0); ++i) { + v.insert(v.begin() + i, i); + } + benchmark::DoNotOptimize(v); + } +} + +// 测试 std::list 的插入速度 +static void BM_listInsert(benchmark::State& state) { + for (auto _ : state) { + std::list l; + for (size_t i = 0; i < state.range(0); ++i) { + l.insert(l.begin(), i); + } + benchmark::DoNotOptimize(l); + } +} +``` + +主入口调用: + +- Range(8, 8 << 10) 表示从8开始,每次乘以8,直到8192,这个对应上面的state.range(0),会把这个数作为参数传递进去 +- BENCHMARK_MAIN表示使用google benchmark的main函数,我们知道每个可执行文件都有一个main,那这个文件可以执行的原因就在于,这个宏展开后就是main函数,你学会了吗? + +```cpp +// 定义基准测试 +BENCHMARK(BM_vectorInsert)->Range(8, 8<<10); +BENCHMARK(BM_listInsert)->Range(8, 8<<10); + +// 运行基准测试 +BENCHMARK_MAIN(); +``` + +## 3.总结 + +本节Step by Step带大家学习google benchmark,并将其应用到我们自己的项目中,你可以在里面做很多有趣的事情,本节源码放入知识星球当中供大家学习,欢迎加入一起交流~ \ No newline at end of file diff --git a/site_extras/cpp-projects-modern-cpp-http-server.md b/site_extras/cpp-projects-modern-cpp-http-server.md new file mode 100644 index 0000000..db7cc5c --- /dev/null +++ b/site_extras/cpp-projects-modern-cpp-http-server.md @@ -0,0 +1,37 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-projects/modern-cpp-http-server/) 的文章,由站点同步脚本导出。 + +# C++20 协程 HTTP 服务器 + +## 现代 C++ HTTP 服务器 + +最近星球小伙伴总是问我有没有网络相关的项目,于是写了一个基于现代C++ + CMake管理的完整项目。 + +该项目具备以下特性: + +- 大量现代C++特性 + - C++20相关 + - cortouine + - concept + - std::format + - std::span + - C++17相关 + - if语句初始化 + - 结构化绑定 + - optional +- 高效的事件驱动机制 + - 使用 `epoll` 实现事件循环,管理大量的客户端连接。`epoll` 是 Linux 平台上处理高并发 I/O 的高效工具,它可以同时监听多个文件描述符的事件,从而有效地减少系统调用开销,提高 I/O 操作的并发性。 + - 服务器在主事件循环中通过非阻塞 `epoll_wait` 监听连接事件,避免了传统阻塞 I/O 带来的性能瓶颈。 +- 协程与异步编程 + - 采用 C++20 协程简化异步任务的实现,提供了类似同步代码的写法,使代码逻辑更加清晰。项目中通过 `co_await` 异步读取和写入客户端数据,减少了线程上下文切换的开销。 + - 实现了 `SocketAwaiter` 和 `AsyncSocketOperation` 两个类,用于封装套接字的异步读写操作,使得编写异步处理代码变得更加简洁和自然。 +- 文件 I/O 操作的异步处理 + - 服务器支持异步读取 HTML 文件内容,通过 `std::async` 实现异步文件读取,避免阻塞主线程,从而更快地响应客户端请求。 + - 在处理请求时,异步读取所需的文件并缓存到内存,减少了 I/O 操作对性能的影响。 +- 非阻塞套接字操作 + - 使用 `fcntl` 将套接字设置为非阻塞模式,保证 `accept` 和 `read/write` 操作不会阻塞服务器的主线程。这在高并发场景下尤为重要,可以显著提高服务器的并发处理能力。 + +项目效果: + + + +学习方式:有详细的文档+代码,可以单独购买加下方微信或者加入星球即可。 \ No newline at end of file diff --git a/site_extras/cpp-projects-protobuf-zerocopy-cpp.md b/site_extras/cpp-projects-protobuf-zerocopy-cpp.md new file mode 100644 index 0000000..4fee3bd --- /dev/null +++ b/site_extras/cpp-projects-protobuf-zerocopy-cpp.md @@ -0,0 +1,180 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-projects/protobuf-zerocopy-cpp/) 的文章,由站点同步脚本导出。 + +# Protobuf ZeroCopy 序列化 + +## 1.ZeroCopyStream + +protobuf在io接口上有一个叫做ZeroCopyStream,对于IO的接口设计,pb提供了相关序列化与反序列化接口,如: + +```cpp +// Read a protocol buffer from the given zero-copy input stream. If +// successful, the entire input will be consumed. +bool ParseFromZeroCopyStream(io::ZeroCopyInputStream* input); + +// Write the message to the given zero-copy output stream. All required +// fields must be set. +bool SerializeToZeroCopyStream(io::ZeroCopyOutputStream* output) const; +``` + +ZeroCopyStream设计的初衷是最小化buffer的拷贝次数,即省略掉stream内部数据拷贝到用户buffer。因此,stream可以返回一个缓冲区,该缓冲区实际上直接指向要存储字节的最终数据结构,并且调用者可以直接与该缓冲区交互,从而消除了中间复制操作。 + +例如:经典的IO stream: + +```cpp +char buffer[[]BUFFER_SIZE]; +input->Read(buffer, BUFFER_SIZE); +DoSomething(buffer, BUFFER_SIZE); +``` + +然后,stream基本上只是调用memcpy()将数据从pb复制到用户buffer中。如果使用ZeroCopyInputStream,我们只需要: + +```cpp +const void* buffer; +int size; +input->Next(&buffer, &size); +DoSomething(buffer, size); +``` + +这里不执行拷贝,调用者最终直接从buffer中读取。 + +ZeroCopyStream提供了两个基类:ZeroCopyOutputStream/ZeroCopyInputStream。 + +以InputStream为例,通常我们可以通过继承的方式自定义自己的ZerCopyStream,需要实现下面四个接口。 + +```cpp +// implements ZeroCopyInputStream ---------------------------------- +bool Next(const void** data, int* size); +void BackUp(int count); +bool Skip(int count); +int64 ByteCount() const; +``` + +一些rpc框架基本都自定义自己的Stream,例如:sofa-pbrpc + +> https://github.com/baidu/sofa-pbrpc/blob/master/src/sofa/pbrpc/buffer.h + +## 2.Demo + +定义一个pb协议,例如:授权验证: + +```cpp +syntax = "proto3"; + +message BasicAuth { + string username = 1; + string password = 2; +} +``` + +随后编写序列化与反序列化: + +- 序列化 + +```cpp +BasicAuth auth_message; +auth_message.set_username("user123"); +auth_message.set_password("password"); + +StringOutputStream output_stream(&buf); + +auth_message.SerializeToZeroCopyStream(&output_stream); +``` + +- 反序列化 + +```cpp +ArrayInputStream input_stream(buf.data(), buf.size()); +BasicAuth auth_message; +if (!auth_message.ParseFromZeroCopyStream(&input_stream)) { + std::cerr << "Failed to parse data." << std::endl; +} + +std::cout << "Username: " << auth_message.username() << std::endl; +std::cout << "Password: " << auth_message.password() << std::endl; +``` + +随后我们便可以调用它: + +```cpp +int main() { + std::string buf; + buf.reserve(512); + ser(buf); + deser(buf); + return 0; +} +``` + +输出: + +```cpp +Username: user123 +Password: password +``` + +本节完,以上完整代码见知识星球,欢迎与我共同探讨rpc相关内容。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` +(gdb) p *dsr +$8 = {typeInfo = {datumlen = 4, typid = 1082, align = 105 'i', byval = true}, datumStreamVersion = DatumStreamVersion_Original, nth = -1, + logical_row_count = 16382, datump = 0x3840470 "\277\377\377\377\231\002", physical_datum_index = -1, physical_datum_count = 0, + datum_beginp = 0x3840470 "\277\377\377\377\231\002", datum_afterp = 0x38503dc "", has_null = true, buffer_beginp = 0x383fc60 "", + null_bitmap = {buffer = 0x383fc70 "", byteBit = 0 '\000', bytePointer = 0x383fc70 "", bitCount = 16382, bitPosition = -1, + readBitOnCount = 58981488}, null_bitmap_beginp = 0x0, rle_can_have_compression = false, rle_compress_beginp = 0x0, rle_repeatcountsp = 0x0, + delta_beginp = 0x0, delta_deltasp = 0x0, delta_datum_p = 0, rle_norepeats_null_bitmap_count = 0, rle_compress_bitmap_count = 0, + rle_repeatcounts_count = 0, rle_repeatcounts_size = 0, delta_item = false, delta_bitmap_count = 0, deltas_count = 0, deltas_size = 0, + rle_block_was_compressed = false, rle_compress_bitmap = {buffer = 0x0, byteBit = 0 '\000', bytePointer = 0x0, bitCount = 0, bitPosition = 0, + readBitOnCount = 0}, rle_repeatcounts_index = 0, rle_in_repeated_item = false, rle_repeated_item_count = 0, + rle_total_repeat_items_read = 0, delta_block_was_compressed = false, delta_bitmap = {buffer = 0x0, byteBit = 0 '\000', + bytePointer = 0xff6c00454244 , bitCount = 0, bitPosition = 0, + readBitOnCount = 13651600}, eyecatcher = "\000\000\000", physical_data_size = 0, maxDataBlockSize = 13652112, + errdetailCallback = 0x35daeb0, errdetailArg = 0x34c4e18, errcontextCallback = 0x34c4e18, errcontextArg = 0x383fc60, memctxt = 0x1077c} +``` \ No newline at end of file diff --git a/site_extras/cpp-projects-pybind11-python-cpp.md b/site_extras/cpp-projects-pybind11-python-cpp.md new file mode 100644 index 0000000..a5280ef --- /dev/null +++ b/site_extras/cpp-projects-pybind11-python-cpp.md @@ -0,0 +1,157 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-projects/pybind11-python-cpp/) 的文章,由站点同步脚本导出。 + +# pybind11 加速 Python + +通常像一些耗时的操作,我们期望在C++中去实现,然后使用Python去调用对应的接口,或者因为底层库的原因,需要支持对外的Python API,那么我们通常需要支持在Python中访问C++,如何实现呢? + +方法比较多,本节以pybind11为例,引入一个完整的项目工程模版,如果你后续有这种需求,可以基于模版去修改。 + +注:(懒人版)本节的所有代码也都放在星球里面。 + +## 1.宏展开 + +假设:我们以比较耗时的排序为例,如果我们想在C++中写一个排序,然后在Python中去使用它,怎么去写呢? + +C++程序是比较简单的,对外暴露一个接口里面做排序即可,例如: + +```cpp +std::vector sort_vector(const std::vector& input) { + std::vector result = input; + std::sort(result.begin(), result.end()); + return result; +} +``` + +那么问题来了,对于Pybind11来说,如何使用这个接口呢? + +使用pybind11的宏即可定义一个模块,例如: + +```cpp +PYBIND11_MODULE(sort_module, m) { + m.doc() = "pybind11 example plugin"; // Optional module docstring + + m.def("sort_vector", &sort_vector, "A function that sorts a list of integers"); +} +``` + +宏展开之后是:PyInit_sort_module + +当我们在python当中import xxx库的时候,Python 解释器会去加载一个 C/C++ 编写的扩展模块时自动调用的入口函数,而这个入口函数就是下面的PyInit_sort_module: + +```cpp +static ::pybind11::module_::module_def pybind11_module_def_sort_module [[maybe_unused]]; + +[[maybe_unused]] static void pybind11_init_sort_module(::pybind11::module_ &); + +extern "C" [[maybe_unused]] __attribute__((visibility("default"))) PyObject *PyInit_sort_module(); + +extern "C" __attribute__((visibility("default"))) PyObject *PyInit_sort_module() { + { + const char *compiled_ver = "3" "." "11"; + const char *runtime_ver = Py_GetVersion(); + size_t len = std::strlen(compiled_ver); + + if (std::strncmp(runtime_ver, compiled_ver, len) != 0 || (runtime_ver[len] >= '0' && runtime_ver[len] <= '9')) { + PyErr_Format(PyExc_ImportError, + "Python version mismatch: module was compiled for Python %s, " + "but the interpreter version is incompatible: %s.", + compiled_ver, runtime_ver); + return nullptr; + } + } + + pybind11::detail::get_internals(); + auto m = ::pybind11::module_::create_extension_module("sort_module", nullptr, &pybind11_module_def_sort_module); + + try { + pybind11_init_sort_module(m); + return m.ptr(); + } catch (pybind11::error_already_set & e) { + pybind11::raise_from(e, PyExc_ImportError, "initialization failed"); + return nullptr; + } catch (const std::exception &e) { + ::pybind11::set_error(PyExc_ImportError, e.what()); + return nullptr; + } +} + +void pybind11_init_sort_module(::pybind11::module_ & m) { + m.doc() = "pybind11 example plugin"; + + m.def("sort_vector", &sort_vector, "A function that sorts a list of integers"); +} + +``` + +所以,如果我们不去使用这个宏,在import库的时候会报错: + +```python +>>> import sort_module +Traceback (most recent call last): + File "", line 1, in +ImportError: dynamic module does not define module export function (PyInit_sort_module) +``` + +## 2.CMake + pybind11 + +pybind11在C++中的使用如下: + +- 头文件 + +```cpp +#include +#include +``` + +- 宏 + +``` +PYBIND11_MODULE(sort_module, m) { + // 你的实现 +} +``` + +那么如何在cmake中下载并链接pybind11呢? + +只需要使用FetchContent模块即可,例如: + +```cpp +FetchContent_Declare( + pybind11 + URL https://github.com/pybind/pybind11/archive/v2.12.0.tar.gz + ) +``` + +然后在CMakeLists中加入你的模块即可: + +```cpp +pybind11_add_module(sort_module sort_module.cc) +``` + +cmake将会帮你下载pybingd11,同时编译好。 + +## 3.使用 + +上面会生成一个so,install到python的site-packages目录,然后import的时候便会识别,如下演示我们自定义的模块: + +```python +import sort_module + +def test_sort(): + input_list = [5, 2, 3, 1, 4] + sorted_list = sort_module.sort_vector(input_list) + print(f"Original list: {input_list}") + print(f"Sorted list: {sorted_list}") + +if __name__ == "__main__": + test_sort() + +``` + +运行: + +``` +(base) ➜ build python ../test.py +Original list: [5, 2, 3, 1, 4] +Sorted list: [1, 2, 3, 4, 5] +``` \ No newline at end of file diff --git a/site_extras/cpp-projects-stack-calculator-design.md b/site_extras/cpp-projects-stack-calculator-design.md new file mode 100644 index 0000000..9b0d197 --- /dev/null +++ b/site_extras/cpp-projects-stack-calculator-design.md @@ -0,0 +1,75 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-projects/stack-calculator-design/) 的文章,由站点同步脚本导出。 + +# C++ 栈式计算器设计 + +写一个栈计算器,设计如下: + +- 支持 `+`、`-`、`*`、`/`运算 +- 支持后缀输入 + +例如: +``` +2 3 + +``` +输出: +``` +5 +``` +如何实现呢? + +其中最关键两个问题: + +1.如何分词 + +2.如何实现栈 + +## 1.如何分词 + +首先定义好输入是一个字符串,空格风格,我们便可以使用istringstream分割每一个token了,基本框架为: + +```cpp +std::istringstream iss(expression); +std::string token; +while (iss >> token) { +} +``` + +## 2.如何实现栈 + +对于计算器来说,最核心就是入栈、出栈。 + +- 碰到数字入栈 +- 碰到操作符出栈计算 + +我们便可以写出框架: + +```cpp +if (isNumber(token)) { + pushNumber(token); +} else if (isOperator(token)) { + popAndCal(token); +} else { + throw std::runtime_error("Invalid token in expression."); +} +``` + +popAndCal是计算的核心,如: + +```cpp +void popAndCal(const std::string& token) { + if (operandStack.size() < 2) { + throw std::runtime_error("Invalid expression."); + } else { + double operand2 = operandStack.top(); + operandStack.pop(); + double operand1 = operandStack.top(); + operandStack.pop(); + double result = performOperation(operand1, operand2, token); + operandStack.push(result); + } +} +``` + +那么我们在栈中变存储了我们想要的结果,最后返回top即可。 + +是不是很简单,欢迎与我一起探讨。 \ No newline at end of file diff --git a/site_extras/cpp-things-cpp-const-family.md b/site_extras/cpp-things-cpp-const-family.md new file mode 100644 index 0000000..33be052 --- /dev/null +++ b/site_extras/cpp-things-cpp-const-family.md @@ -0,0 +1,116 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/cpp-const-family/) 的文章,由站点同步脚本导出。 + +# C++ const 全家桶 + +大纲 + +- const +- constexpr +- C++20 + - consteval + - constinit + +**注:本文所有代码示例已放星球。** + +## 1.const + +`const` 是我们在学习 C++/C 时最早接触的关键字之一,通常作为 `cv-qualifier` 使用,表示对象不可变。它并不保证 **编译时评估**,具体何时评估由编译器决定。例如: + +```cpp +const int a = 1; // 编译时 +const auto x = getdata(); // 运行时 +``` + +const有时可以用于编译时常量表达式,但是有一些例外,例如: + +```cpp +const double count_1 = 3.3; +std::array(count_1)> moreDoubles1 {1.1, 2.2, 3.3}; +``` + +此时便会报错:the value of 'count_1' is not usable in a constant expression + +但是这样是正常的: + +```cpp +const int count = 3; +std::array doubles {1.1, 2.2, 3.3}; +``` + +本质在于编译时表达式求值,于是C++11引入了constexpr。 + +## 2.constexpr + +constexpr 可以用来指定变量的值可以出现在常量表达式,此时只需要将前面例子的const变为constexpr,便可以正常运行。 + +```cpp +constexpr double count_2 = 3.3; +std::array(count_2)> moreDoubles2 {1.1, 2.2, 3.3}; +``` + +`constexpr` 并不一定只能在编译时执行。它的本质含义是 **可以** 在编译时计算,但如果在运行时使用 `constexpr` 变量或函数,它也可以在运行时执行。 + +例如:result1与result2完全不一样,result1是编译时计算,但是result2是运行时计算的,抛个疑问:**你知道为什么吗?** + +所以并不是指定了constexpr的函数它就一定是编译时计算的。 + +```cpp +constexpr int compileTimeSum(int a, int b) { + return a + b; +} + +int main() { + constexpr int result1 = compileTimeSum(3, 5); + int x = 10; + int result2 = compileTimeSum(x, 20); + return 0; +} +``` + +那么谁能保证呢?于是C++20来了。 + +## 3.C++20 + +### 3.1 consteval + +consteval解决了上面的问题,它只能用于函数,并强制所有调用在编译时进行。如果我们此时将上面的constexpr改为consteval,此时result2会发生什么呢? + +那便是报错:call to consteval function '**compileTimeSum(x, 20)**' is not a constant expression + +```cpp +consteval int compileTimeSum(int a, int b) { + return a + b; +} + +int main() { + constexpr int result1 = compileTimeSum(3, 5); + int x = 10; + int result2 = compileTimeSum(x, 20); + return 0; +} +``` + +如果使用了consteval使用在变量上,也会报错:**error:** a variable cannot be declared '**consteval**'。 + +> https://en.cppreference.com/w/cpp/language/consteval + +### 3.2 constinit + +constinit可以强制静态或线程局部变量进行常量初始化。 + +- 不能与 constexpr、consteval 一起使用 +- 一个非类型说明符,作用是assert +- 保证变量是静态初始化 +- 只能用于 static 或者 thread_local + +例如:下面会编译报错:error: 'constinit' can only be applied to a variable with static or thread storage duration + +```cpp +constinit int a = 100; +int main() { + ++a; + constinit auto b = 100; // error + // b has thread storage duration + constinit thread_local auto b = 100; +} +``` \ No newline at end of file diff --git a/site_extras/cpp-things-cpp-dereference-operator.md b/site_extras/cpp-things-cpp-dereference-operator.md new file mode 100644 index 0000000..d3e0b90 --- /dev/null +++ b/site_extras/cpp-things-cpp-dereference-operator.md @@ -0,0 +1,78 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/cpp-dereference-operator/) 的文章,由站点同步脚本导出。 + +# C++ 解引用运算符 * + +## 0.导语 + +通常我们在操作指针时,会使用到`*`,例如: + +```cpp +int a = 1; +int *ptr = a; +std::cout << *ptr << std::endl; +``` + +除此之外,还有什么用途? + +答案是肯定的,例如,在C++中还有下面三类: + +- `::*` +- `.*` +- `->*` + +那么你知道这三个分别表示什么吗? + +## 1.::* + +`::*`被称之为**Pointer to member declarator**,指向成员声明符的指针。 + +举个例子吧: + +下面这个例子你如何区w_的地址呢? + +如果你写成`double * d = &w_`; 它便是无效的,因为不知道`w_`在哪里哦,所以需要指向成员声明符的指针,这样便可以进行绑定,有了这个指针之后,便可以访问成员了,继续讲后面两个。 + +```cpp +class Rectangle { + public: + double w_, h_; + + Rectangle(double w, double h) : w_(w), h_(h) {} + double area() { return w_ * h_; } +}; + +int main() { + Rectangle r{2.1, 3.2}; + double Rectangle::* w = &Rectangle::w_; + r.*w = 2.2; + std::cout << r.w_ << std::endl; +}; +``` + +## 2..* + +`.*`被称为pointer to member of object,这个比较好理解,就是访问**对象**成员的指针,指向成员的指针允许你引用类对象的非静态成员。不能使用指向成员的指针指向静态类成员,因为静态成员的地址与任何特定对象都没有关联。 + +所以看上面的例子中有了这一行代码,修改成员w的值为2.2。 + +```cpp +r.*w = 2.2; +``` + +## 3.->* + +`->*`被称为pointer to member of pointer,这个就更好理解了,跟.*类似,不过操作的是**指针对象**成员的地址, + +所以对应的例子变为: + +```cpp +Rectangle r{2.1, 3.2}, *pr; +pr = &r; +std::cout << pr->*w << std::endl; +``` + +更加详细的请阅读cppreference.com。 + +> https://en.cppreference.com/w/cpp/language/pointer#Pointers_to_members + +你学会了吗?本节完~ \ No newline at end of file diff --git a/site_extras/cpp-things-cpp-iv-initialization.md b/site_extras/cpp-things-cpp-iv-initialization.md new file mode 100644 index 0000000..6eaa058 --- /dev/null +++ b/site_extras/cpp-things-cpp-iv-initialization.md @@ -0,0 +1,83 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/cpp-iv-initialization/) 的文章,由站点同步脚本导出。 + +# C++ 初始化与赋值 + +## C++ iv + +```cpp +int* p = nullptr; +std::shared_ptr s(p); // 计数器1 +std::shared_ptr s2(s); // 计数器2 +assert(s.use_count() == 2); +``` + +这段代码会引发几个疑问: + +- 为何nullptr原始指针构造出来的智能指针计数器不是0? +- delete nullptr是否是安全的? + +### 1.空的shared_ptr语义 + +我们直接看shared_ptr源码,所谓**空**指的是: + +- 从默认构造一个shared_ptr +- 从nullptr_t构造一个shared_ptr + +这两种情况才是空,而对于从原始指针构造的shared_ptr,它占有那个指针的所有权,不管它是否为空。 + +分别对应下面三个构造。 + +```cpp +private: + element_type* __ptr_; + __shared_weak_count* __cntrl_; + +public: + _LIBCPP_HIDE_FROM_ABI + _LIBCPP_CONSTEXPR shared_ptr() _NOEXCEPT + : __ptr_(nullptr), + __cntrl_(nullptr) + { } + + _LIBCPP_HIDE_FROM_ABI + _LIBCPP_CONSTEXPR shared_ptr(nullptr_t) _NOEXCEPT + : __ptr_(nullptr), + __cntrl_(nullptr) + { } + shared_ptr(_Yp* __p, _Dp __d) + : __ptr_(__p) + {} +``` + +在上面原始指针式nullptr情况下,删除delete nullptr会发生什么?删除两次又回发生什么? + +### 2.delete nullptr + +假设有下面的代码,运行之后会crush?会正常运行? + +```cpp +#include + +int main() { + int *p = nullptr; + delete p; + std::cout << "delete once" << std::endl; + delete p; + std::cout << "delete twice" << std::endl; + +} +``` + +答案是:正常输出。 + +具体原因参考cppreference: + +If *expression* evaluates to a null pointer value, no destructors are called, and the deallocation function may or may not be called (it's unspecified), but the default deallocation functions are guaranteed to do nothing when passed a null pointer. + +> https://en.cppreference.com/w/cpp/language/delete + +如果表达式的计算结果为空指针值,则不会调用析构函数,也可能调用或不调用释放函数(未指定),但默认的释放函数保证在传递空指针时不做任何事情。 + + +!!! note "阅读提示" + 本文讨论 C++ 中**初始化**(initialization)与**赋值**(assignment)的区别,可参见 [类初始化列表](../cpp-idioms/class-initializer-list.md)。 \ No newline at end of file diff --git a/site_extras/cpp-things-cpp-multiple-return-values.md b/site_extras/cpp-things-cpp-multiple-return-values.md new file mode 100644 index 0000000..0e8697e --- /dev/null +++ b/site_extras/cpp-things-cpp-multiple-return-values.md @@ -0,0 +1,147 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/cpp-multiple-return-values/) 的文章,由站点同步脚本导出。 + +# C++ 多返回值写法 + +[TOC] + +今天再提出一个比较经典的面试题:函数如何返回多个结果?你能想到几种办法? + +其实这道题目考察的知识点非常的多,涉及到: + +- 指针 +- 引用 +- lambda +- functional +- tie +- tuple +- struct bingding +- template deduce +- 等等 + +本篇文章将会给大家用不同的方法来解答这道题目,首先我们转化一下这个题目,使用`a / b`来模拟(下面不考虑被除数为0的场景),返回商与余数。简单来说转化为一个函数返回商与余数有多少种办法? + +下面进入正文,注:本节的所有代码已首发于星球,感兴趣戳下方二维码即可。 + +## 1. 指针与引用 + +对接口进行更改,添加两个变量,使用指针与引用。 + +```cpp +void divideWithReferences(int dividend, int divisor, int& quotient, int& remainder) { + quotient = dividend / divisor; + remainder = dividend % divisor; +} + +void divideWithPointers(int dividend, int divisor, int* quotient, int* remainder) { + if (quotient) *quotient = dividend / divisor; + if (remainder) *remainder = dividend % divisor; +} + +``` + +使用这种方式的缺点也明显可见,当返回值不止两个,接口就非常的长。此外,如果面试官要求你不允许更改函数接口,那么这个办法用不了了。 + +## 2. Tuple + Tie + +Tuple和tie的组合为函数返回多个值提供了一种简洁的方式。通过使用`std::tie`,我们可以将tuple中的元素解包到不同的变量中,提高代码的清晰度。 + +这种方法算是标准答案之一,比较简单清晰,如果返回值是两个pair也可以。 + +```cpp +std::tuple divide(int dividend, int divisor) { + return std::make_tuple(dividend / divisor, dividend % divisor); +} + + +std::tie(quotient, remainder) = divide(14, 3); +std::cout << quotient << ", " << remainder << std::endl; +``` + +## 3. Struct Binding + +对上面进行改造,C++17引入的结构化绑定进一步简化了多值返回的代码。使用auto和结构化绑定,使得代码更加直观易懂。 + +如果面试官要你进一步的对上面的方法进行改造,那么可以变为下面这个代码示例,使用结构化绑定使代码更加优雅。 + +```cpp +auto divide(int dividend, int divisor) { + struct result { + int quotient; + int remainder; + }; + return result{dividend / divisor, dividend % divisor}; +} +auto [quotient, remainder] = divide(14, 3); +``` + +## 4. 函数 Callback + +在日常项目中,特别是一些开源项目,callback是一种常用的手段,通过传递处理返回值的callback,让用户自定义处理,这样便实现了返回多个值,实现更加灵活的代码结构。这对于异步编程和事件处理等场景非常有用。 + +```cpp +void divide(int dividend, int divisor, std::function callback) { + callback(dividend / divisor, dividend % divisor); +} +``` + +## 5. 自定义 Out + +自定义out是一种通过结构体包装的方式,将输出参数作为结构体的成员。这种方式提高了代码的可读性,尤其适用于需要返回多个值的函数。 + +```cpp +template +struct out { + std::function target; + + out(T* t) + : target([t](T&& in) { + if (t) *t = std::move(in); + }) {} + + template + void emplace(Args&&... args) { + target(T(std::forward(args)...)); + } + + template + void operator=(X&& x) { + emplace(std::forward(x)); + } + + template + void operator()(Args&&... args) { + emplace(std::forward(args)...); + } +}; + +void divide(int dividend, int divisor, out& quotient_out, out& remainder_out) { + quotient_out.emplace(dividend / divisor); + remainder_out.emplace(dividend % divisor); +} +``` + +## 6. 模版推导 + +C++的模版推导为开发者提供了更为灵活和简洁的代码编写方式。通过模版推导,我们可以处理不同类型的数据而无需显式指定类型。 + +```cpp +template +struct many { + T1 quotient; + T2 remainder; +}; + +template +many(T1, T2) -> many; + +many divide(int dividend, int divisor) { + return many{ + dividend / divisor, + dividend % divisor, + }; +} + +auto [quotient, remainder] = divide(14, 3); +``` + +总体而言,C++提供了多种方式来实现多值返回,每种方式都有其适用的场景。选择合适的方式取决于具体的需求,开发者可以根据代码结构和可读性来灵活使用这些特性。 \ No newline at end of file diff --git a/site_extras/cpp-things-cpp-mutable-keyword.md b/site_extras/cpp-things-cpp-mutable-keyword.md new file mode 100644 index 0000000..0dd5d82 --- /dev/null +++ b/site_extras/cpp-things-cpp-mutable-keyword.md @@ -0,0 +1,75 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/cpp-mutable-keyword/) 的文章,由站点同步脚本导出。 + +# C++ mutable 关键字 + +### 深入探讨 C++ 中的 `mutable` 关键字 + +在 C++ 中,`mutable` 关键字经常被认为是“穿透”常量(`const`)限制的工具。在某些情况下,虽然我们希望对一个对象施加常量保护,但还是需要对该对象的某些数据成员进行修改。本文将详细探讨 `mutable` 的使用场景和背后的设计哲学,并讨论其如何在 C++ 中解决特定的编程需求。 + +#### 1. `mutable` 关键字的基本用途 + +我们知道,当一个成员函数被 `const` 修饰时,该成员函数保证不会修改对象的状态。然而,在实际编程中,有时我们希望修改一个数据成员,但仍然希望将成员函数标记为 `const`。这就是 `mutable` 关键字的作用: + +```cpp +class Foo +{ +private: + mutable bool done_; +public: + void doSomething() const { + // 在 const 成员函数中修改 mutable 成员 + done_ = true; + } +}; +``` + +在上面的例子中,`doSomething()` 被声明为 `const`,表明它不会改变对象的状态。然而,`done_` 被标记为 `mutable`,这意味着即使在 `const` 方法中,我们仍然可以修改 `done_`。这种设计使得我们可以在保持函数接口语义不变的情况下,管理某些需要修改的数据。 + +#### 2. 逻辑常量性 vs. 位常量性 + +C++ 中的常量性可以分为两类:**逻辑常量性** 和 **位常量性**。逻辑常量性指的是对象的状态在外部看来没有发生变化,而位常量性指的是对象在内存中的位数据保持不变。`mutable` 关键字允许我们定义逻辑常量性,即即使对象的某些内部数据变化了,但在逻辑上对象的状态并未发生可见变化。 + +比如在一个线程安全的类中,我们可能会使用 `boost::mutex` 来保护数据访问。为了在 `const` 方法中获取锁,我们可以将 `mutex` 声明为 `mutable`: + +```cpp +class ThreadSafeClass { +private: + mutable std::mutex mtx_; + int data_; +public: + int getData() const { + std::lock_guard lock(mtx_); + return data_; + } +}; +``` + +在上述例子中,虽然 `getData` 方法被声明为 `const`,但我们需要锁来保护数据访问的线程安全性。因此,我们将 `mtx_` 声明为 `mutable`,以允许在 `const` 方法中对其进行修改。这种情况下,我们说类的逻辑状态没有改变,只是为确保线程安全而进行的修改。 + +#### 3. 在 Lambda 表达式中的 `mutable` + +从 C++11 开始,`mutable` 也可以用于 lambda 表达式,以允许修改按值捕获的变量。默认情况下,lambda 中按值捕获的变量是不可变的。如果需要在 lambda 中修改捕获的变量,可以使用 `mutable`: + +```cpp +int x = 0; +auto f1 = [=]() mutable { x = 42; }; // OK: 可以修改捕获的 x +auto f2 = [=]() { x = 42; }; // Error: 按值捕获的变量不能被修改 +``` + +这种情况下,`mutable` 允许我们在函数体内修改捕获的变量,而不会影响到外部变量的值。 + +#### 4. 避免使用 `const_cast` + +没有 `mutable` 关键字的情况下,我们可能需要使用 `const_cast` 来处理这些特殊情况。`const_cast` 可以去掉对象的常量性,但它也有缺点:`const_cast` 可以完全移除常量性保护,让我们对对象进行任意的修改和方法调用,这会导致代码的可读性和安全性下降。 + +相比之下,`mutable` 关键字提供了一种更细粒度的控制方式。通过 `mutable`,我们可以在特定的数据成员上设置“例外”,允许在 `const` 方法中修改,而不必将整个对象的常量性去掉。因此,`mutable` 能够更安全地处理这些特例。 + +#### 5. 使用 `mutable` 的设计哲学 + +`mutable` 关键字的设计哲学在于保持接口的**逻辑常量性**。在大多数情况下,我们希望编写 const-correct 的代码,这意味着对象的状态在没有显式修改的情况下不会变化。`mutable` 允许我们在某些内部实现上做出让步,比如缓存、引用计数、调试信息等,而不会打破这种接口设计的一致性。 + +设计一个接口时,我们应该明确何时以及为什么需要修改一个对象的状态。`mutable` 关键字能让代码在逻辑层面保持不变,同时又提供了修改特定数据成员的能力。这需要设计者深思熟虑,确保 `mutable` 的使用不会破坏对象的常量性概念,只有在确实需要的情况下使用。 + +### 6.参考资料 + +> https://stackoverflow.com/questions/105014/does-the-mutable-keyword-have-any-purpose-other-than-allowing-a-data-member-to \ No newline at end of file diff --git a/site_extras/cpp-things-cpp-restrict-optimization.md b/site_extras/cpp-things-cpp-restrict-optimization.md new file mode 100644 index 0000000..e6850c6 --- /dev/null +++ b/site_extras/cpp-things-cpp-restrict-optimization.md @@ -0,0 +1,96 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/cpp-restrict-optimization/) 的文章,由站点同步脚本导出。 + +# C++ restrict 与编译优化 + +## restrict性能优化 + +```cpp +#include +#include +#include + +#define LEN 1<<20 +using AggregateDataPtr = char *; + +struct AvgState { + uint64_t numerator{0}; + uint64_t denominator{0}; + + double divide() { return numerator / denominator; } +}; + +class Column { +public: + std::vector & data() {return data_; } + +private: + std::vector data_; +}; + +template +class IAggregate { +public: + void addBatch(size_t batch_size, AggregateDataPtr state, Column *args) __attribute__((noinline)) { + for (size_t i = 0; i < batch_size; ++i) { + static_cast(this)->add(state, args, i); + } + } + + void addBatchWithoutOpt(size_t batch_size, AggregateDataPtr state, Column *args) __attribute__((noinline)) { + for (size_t i = 0; i < batch_size; ++i) { + static_cast(this)->addWithoutOpt(state, args, i); + } + } +}; + +class AvgAggregator final : public IAggregate { +public: + static AvgState& data(AggregateDataPtr __restrict place) { return *reinterpret_cast(place); } + static AvgState& data_without_opt(AggregateDataPtr place) { return *reinterpret_cast(place); } + + void add(AggregateDataPtr __restrict state, Column * args, uint64_t row_num) const { + data(state).numerator += args[0].data()[row_num]; + ++(data(state).denominator); + } + + void addWithoutOpt(AggregateDataPtr state, Column * args, uint64_t row_num) const { + data_without_opt(state).numerator += args[0].data()[row_num]; + ++(data_without_opt(state).denominator); + } +}; + +static void BM_loop_with_optimize(benchmark::State& state) { + AvgAggregator avgAggregator; + Column column; + for (size_t i = 0 ; i < LEN; i++) { + column.data().push_back(i); + } + Column arg0[] = {column}; + // Code inside this loop is measured repeatedly + for(auto _ : state) { + auto avgState = std::make_unique(); + avgAggregator.addBatch(LEN, reinterpret_cast(avgState.get()), arg0); + } +} + +static void BM_loop_without_optimize(benchmark::State& state) { + AvgAggregator avgAggregator; + Column column; + for (size_t i = 0 ; i < LEN; i++) { + column.data().push_back(i); + } + Column arg0[] = {column}; + // Code inside this loop is measured repeatedly + for(auto _ : state) { + auto avgState = std::make_unique(); + avgAggregator.addBatchWithoutOpt(LEN, reinterpret_cast(avgState.get()), arg0); + } +} + +BENCHMARK(BM_loop_without_optimize); +BENCHMARK(BM_loop_with_optimize); +``` + +> https://quick-bench.com/q/ccjuWr_JcLozAfy3Gxf3uVu76Yk + +https://github.com/ClickHouse/ClickHouse/pull/19946 \ No newline at end of file diff --git a/site_extras/cpp-things-cpp-string-abi-compatibility.md b/site_extras/cpp-things-cpp-string-abi-compatibility.md new file mode 100644 index 0000000..42b4346 --- /dev/null +++ b/site_extras/cpp-things-cpp-string-abi-compatibility.md @@ -0,0 +1,99 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/cpp-string-abi-compatibility/) 的文章,由站点同步脚本导出。 + +# C++ string ABI 兼容性 + +### 项目实战问题:避坑 C++ String ABI 兼容性问题 + +[TOC] + +为什么很多人不喜欢C++ string,反而要自己造轮子呢?其中一个问题就是C++ string abi问题,你遇到过吗? + +之前使用conan与跨平台库的时候发生了C++ string abi兼容问题,本节将会引入ABI的背景与编译器相关的内容,讲讲如何避免这个坑,如果你遇到过,欢迎留言~ + +#### 1. ABI 变更的背景 + +**GCC 5.1**之后加强了对 C++11 的支持,并引入了一些新特性。在这一版本中,libstdc++ 引入了新的库 ABI,更新了 `std::string` 和 `std::list` 的实现。这些变化符合 C++11 标准,后者禁止使用 Copy-On-Write 字符串,并要求链表能够跟踪其大小。 + +实际项目中在使用 Conan 时,开发者可以通过调整 `compiler.libcxx` 来选择使用的 ABI: + +- **libstdc++**:使用旧 ABI。 +- **libstdc++11**:使用新 ABI。 + +选择适当的 ABI 可以确保与其他依赖库的兼容性,特别是在使用不同版本的编译器或库时。 + +#### 2. 编译器的默认设置 + +在使用 GCC 进行编译时,如果没有显式地指定 `--with-default-libstdcxx-abi=gcc4-compatible`,则 GCC 默认使用新的 C++11 ABI。这意味着: + +- 如果你的代码或依赖库是在旧 ABI 下编译的,那么当你尝试链接时,可能会遇到未定义的符号错误。 + +例如,编译以下代码时: + +```cpp +// lib.cc +#include +void foo(const std::string& s) {} +``` + +使用命令: + +```bash +g++-14 -D_GLIBCXX_USE_CXX11_ABI=0 -c lib.cc +``` + +将生成符号 `__Z3fooRKSs`,这是旧 ABI 的格式。 + +然而,当你编译并链接另一个使用新 ABI 的文件时,可能会出现如下错误: + +```bash +g++-14 main.cc lib.o +``` + +``` +Undefined symbols for architecture x86_64: + "foo(std::__cxx11::basic_string, std::allocator > const&)", referenced from: + _main in ccbWGoYh.o +ld: symbol(s) not found for architecture x86_64 +collect2: error: ld returned 1 exit status +``` + +#### 3. 解决方案 + +为了确保编译通过,有以下两种解决方案: + +**方法 1: 手动指定 ABI** + +在编译所有源文件时,确保手动添加 `-D_GLIBCXX_USE_CXX11_ABI=0` 来兼容旧的库,例如: + +```bash +g++-14 -D_GLIBCXX_USE_CXX11_ABI=0 main.cc lib.o +``` + +**方法 2: 使用新的 ABI 编译所有文件** + +如果你愿意使用新的 C++11 ABI,可以在编译库文件时使用 `-D_GLIBCXX_USE_CXX11_ABI=1`,这样两者符号就会匹配: + +```bash +g++-14 -D_GLIBCXX_USE_CXX11_ABI=1 -c lib.cc +g++-14 main.cc lib.o +``` + +此时,链接时符号将会匹配,不再出现未定义符号的错误。 + +#### 4. 符号比较 + +通过 `nm` 命令查看不同 ABI 下的符号,可以直观地看到二者的差异: + +```bash +g++-14 -D_GLIBCXX_USE_CXX11_ABI=0 -c lib.cc +nm lib.o | grep foo +# 输出: 0000000000000000 T __Z3fooRKSs + +g++-14 -D_GLIBCXX_USE_CXX11_ABI=1 -c lib.cc +nm lib.o | grep foo +# 输出: 0000000000000000 T __Z3fooRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE +``` + +#### 5. 小结 + +ABI是C++升级以及编译过程中遇到的一个非常大的坑,希望可以通过这篇文章讲明白这些内容。 \ No newline at end of file diff --git a/site_extras/cpp-things-cpp-template-disambiguator.md b/site_extras/cpp-things-cpp-template-disambiguator.md new file mode 100644 index 0000000..090039f --- /dev/null +++ b/site_extras/cpp-things-cpp-template-disambiguator.md @@ -0,0 +1,57 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/cpp-template-disambiguator/) 的文章,由站点同步脚本导出。 + +# C++ 模板消歧 + +## 1.背景 + +最近看到一段代码: + +```cpp + auto chunk_left = first_sort_key.template GetChunk(left); +``` + +请问,这里的`.template`代表什么意义? + +本节将从实际例子出发,探讨这个意义。 + +## 2.template disambiguator + +在C++中,当使用模板的依赖名称(dependent names)时,有时需要使用模板消除符(template disambiguator)来帮助编译器区分这些名称。所谓"dependent names"是指依赖于模板参数的类型或值,编译器不能在实例化模板之前确定它们的具体含义。这可能会导致编译器在解析这些名称时产生二义性。 + +模板消除符(template disambiguator)使用关键字"template"来明确告诉编译器一个特定的名称是一个模板。这样编译器就可以正确地解析该名称,而不会产生二义性。 + +关键字template只能在操作符::(作用域解析)、->(通过指针访问成员)和.(成员访问),以下都是有效的示例 + +- T::template foo(); +- s.template foo(); +- this->template foo(); +- typename T::template iterator::value_type v; + +官方资料: + +> https://en.cppreference.com/w/cpp/language/dependent_name + +假设有下面这个代码,我们如果不加.template会得到什么? + +```cpp +template +auto foo_is_not_a_template() { + // ((T::foo) < 34) > (::val()) + return T::foo<34>::val(); +} +``` + +上述会被解析为:((T::foo) < 34) > (::val()) + +对应的.template例子为: + +```cpp +template +auto foo_is_a_template() { + return T::template foo<34>::val(); +} +``` + +这样便调用会正确解析为模版,foo<34>。 + +本节完整代码将会在星球公布,感兴趣加入即可,谢谢! \ No newline at end of file diff --git a/site_extras/cpp-things-cpp-vs-rust-destructor.md b/site_extras/cpp-things-cpp-vs-rust-destructor.md new file mode 100644 index 0000000..ff5a845 --- /dev/null +++ b/site_extras/cpp-things-cpp-vs-rust-destructor.md @@ -0,0 +1,58 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/cpp-vs-rust-destructor/) 的文章,由站点同步脚本导出。 + +# C++ 与 Rust 析构对比 + +在Rust中mem::forget用于防止对象的析构,跳过清理工作,从而让资源的释放交给其他机制管理。 + +例如: + +```cpp +let file = File::open("foo.txt").unwrap(); +mem::forget(file); +``` + +那么在C++中如何防止析构?即跳过析构函数呢? + +**本节将给出三种办法,完整代码示例也放于星球。** + +## 1. `placement new` + +`placement new`可以让我们手动管理对象的构造和析构。如果不手动调用对象的析构函数,我们就可以跳过对象的析构,从而实现类似`mem::forget`的效果。 + +例如: + +```cpp +alignas(Foo) std::byte storage[sizeof(Foo)]; // 分配对齐的原始内存块 +Foo* pfoo = new (storage) Foo(); // 在这块内存上构造Foo对象 +``` + +在上面的代码中,`placement new`创建了对象`pfoo`,但我们不调用析构函数,这样可以跳过对象的清理逻辑。 + +## 2. 使用智能指针的自定义删除器 + +另一种实现类似功能的方法是通过自定义智能指针的删除器,防止对象被销毁。我们可以定义一个不执行任何操作的删除器,从而避免析构函数的调用。 + +```cpp +std::unique_ptr pfoo(new Foo(), NoOpDeleter{}); +``` + +使用自定义的删除器`NoOpDeleter`,我们可以防止`std::unique_ptr`在离开作用域时调用析构函数,从而实现类似`mem::forget`的效果。 + +## 3.union + +union的方案很神奇,大家知道原理吗?欢迎留言区一起讨论~ + +```cpp +template +union Wrapper { + T value; + ~Wrapper() {} +}; + +struct Foo { + ~Foo() { std::cout << "dtor foo\n"; } +}; + +Wrapper f; +// f.value.~Foo); 必须手动调用 +``` \ No newline at end of file diff --git a/site_extras/cpp-things-member-initializer-list-notes.md b/site_extras/cpp-things-member-initializer-list-notes.md new file mode 100644 index 0000000..b1a30b2 --- /dev/null +++ b/site_extras/cpp-things-member-initializer-list-notes.md @@ -0,0 +1,82 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/member-initializer-list-notes/) 的文章,由站点同步脚本导出。 + +# C++ 成员初始化列表注意点 + +在C++中,类的正确初始化是确保程序正常运行的关键步骤之一。尤其是在使用初始化列表时,我们需要注意一些潜在的陷阱,以避免在代码执行过程中产生意外的结果。 + +初始化列表通常被认为是设置新类实例状态的首选方法,因为它提供了一种简洁、高效的方式来初始化类成员。然而,有时候在初始化列表中引用其他成员变量可能导致一些令人困惑的问题。 + +## 问题示例 + +考虑以下的`Foo`类: + +```cpp +#include + +class Foo +{ +public: + Foo(int size) : size_(size), + capacity_(size_ + 2), + length_(capacity_) + {} + + void print() { + std::cout << "Size: " << size_ << " Capacity: " << capacity_ << " Length: " << length_ << std::endl; + } + +private: + int length_; + int capacity_; + int size_; +}; + +int main() { + Foo foo(50); + foo.print(); +} +``` + +在这个例子中,我们试图通过初始化列表中引用其他成员变量的方式来初始化`length_`、`capacity_`和`size_`。然而,由于类成员的初始化顺序是按照它们在类中声明的顺序进行的,可能导致`length_`与`capacity_`并没有按照我们期望的方式初始化。 + +## 如何解决? + +为了避免这种问题,应该遵循以下几点: + +**1.避免在初始化列表中引用其他成员变量** + +**2.顺序一致性:成员变量的初始化顺序是按照它们在类中声明的顺序** + +修正示例: + +```cpp +#include + +class Foo +{ +public: + Foo(int size) : size_(size), + capacity_(size_ + 2), + length_(calculateLength(size_)) + {} + + void print() { + std::cout << "Size: " << size_ << " Capacity: " << capacity_ << " Length: " << length_ << std::endl; + } + +private: + int calculateLength(int size) { + return size + 2; + } + +private: + int size_; + int capacity_; + int length_; +}; + +int main() { + Foo foo(50); + foo.print(); +} +``` \ No newline at end of file diff --git a/site_extras/cpp-things-smart-pointer-as-parameter.md b/site_extras/cpp-things-smart-pointer-as-parameter.md new file mode 100644 index 0000000..b5778fc --- /dev/null +++ b/site_extras/cpp-things-smart-pointer-as-parameter.md @@ -0,0 +1,73 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/smart-pointer-as-parameter/) 的文章,由站点同步脚本导出。 + +# C++ 智能指针作函数参数 + +## C++那些事之智能指针何时作为函数参数 + +通常我们可以看到有些函数用了智能指针作为参数,有些函数又使用了裸指针,究竟用哪一个呢?使用原则是什么? + +本节使用abseil的tips进行讲解。 + +如下所示,这段代码有什么问题呢? + +```cpp +bool CanYouPetTheDog(const std::shared_ptr& dog, + absl::Duration min_delay) { + return dog->GetLastPetTime() + min_delay < absl::Now(); +} +``` + +CanYouPetTheDog函数并不会影响其 dog 参数的所有权,但它的签名要求将其存储在 std::shared_ptr 中。这样做会在特定所有权模型上创建一个不必要的依赖,尽管函数本身并不需要这种依赖。这种依赖会阻止调用者使用其他模型,比如 std::unique_ptr 或在堆栈上构造对象。 + +## 使用指针/引用 + +当所有权没有受到影响时使用**引用或指针**。 通过使用引用/指针,我们可以消除对特定所有权模型的依赖,并允许我们的函数与任何类型为 Dog 的对象一起工作。 + +```cpp +bool CanYouPetTheDog(const Dog& dog, absl::Duration min_delay) { + return dog.GetLastPetTime() + min_delay < absl::Now(); +} +``` + +有了上述定义,无论调用者的所有权模型如何,都可以调用该函数: + +```cpp +Dog stack_dog; +if (CanYouPetTheDog(stack_dog, delay)) { ... } + +auto heap_dog = std::make_unique(); +if (CanYouPetTheDog(*heap_dog, delay)) { ... } + +CustomPetPtr custom_dog = CreateDog(); +if (CanYouPetTheDog(*custom_dog, delay)) { ... } +``` + +函数修改了传递的值,则传递可变引用或原始指针,并使用与上述相同的用法。 + +## 使用智能指针 + +在函数修改所有权时使用智能指针,以下代码为不同的智能指针参数提供了几个重载。第一个重载假定接管传递对象的所有权,第二个重载为传递对象添加了一个共享引用。这两种操作取决于调用方如何处理 Dog 的所有权。无法接管位于栈的 Dog,因为无法从栈中取走所有权。 + +```cpp +class Human { + public: + ... + void Adopt(std::unique_ptr dog) { + pets_.push_back(std::move(dog)); + } + void Adopt(std::shared_ptr cat) { + pets_.push_back(std::move(cat)); + } + + private: + std::vector> pets_; + ... +}; + +``` + +## 总结 + +如果所有权没有被转移或修改,则应该避免将智能指针作为函数参数。 + +> https://abseil.io/tips/188 \ No newline at end of file diff --git a/site_extras/cpp-things-std-function-cpp.md b/site_extras/cpp-things-std-function-cpp.md new file mode 100644 index 0000000..d5d5496 --- /dev/null +++ b/site_extras/cpp-things-std-function-cpp.md @@ -0,0 +1,78 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/std-function-cpp/) 的文章,由站点同步脚本导出。 + +# C++ std::function 详解 + +最近在写C++代码时用到了`is_function`,然后顺便看了一下源码实现,发现了一些问题,以前咱们学习的是三个点`...`,那六个点`......`你知道是啥吗? + +同样,如果有一段这样的代码: + +``` +template +struct foo(Args......); +``` + +你知道是啥意思? + +下面我们一起来深入细节! + +## 1.六个点 + +六个点我们通常叫做two ellipsis operators,我们可以将六个点进行拆分,于是得到下面三种情况 + +``` +template void foo1(Args......) +template void foo2(Args... ...) +template void foo3(Args..., ...) +``` + +这下就好看了,我们来看std::is_function的代码。 + +## 2.is_function实现细节 + +`std::is_function`的实现涉及到主模板和两个偏特化: + +```cpp +// 主模板,假设提供的类型不是函数类型 +template +struct is_function : std::false_type {}; + +// 偏特化,用于正常的函数类型 +template +struct is_function : std::true_type {}; + +// 偏特化,用于变参函数类型 +template +struct is_function : std::true_type {}; +``` + +- 主模板假设提供的类型不是函数类型,并将`std::false_type`作为结果。 +- 第一个偏特化用于正常的函数类型,其中`Ret`表示返回类型,`Args...`表示参数类型。如果提供的类型匹配这种函数签名,则`std::true_type`作为结果。 +- 第二个偏特化用于变参函数类型,其中的省略号表示函数具有可变数量的参数。同样,如果提供的类型匹配这种函数签名,则`std::true_type`作为结果。 + +## 3.使用示例 + +最后给出一个`std::is_function`的使用示例: + +```cpp +#include +#include + +int func(int x, int y) { return x + y; } +int vararg_func(int x, ...) { return x; } + +int main() { + std::cout << std::boolalpha; + std::cout << "Is func a function? " << std::is_function::value << std::endl; + std::cout << "Is vararg_func a function? " << std::is_function::value << std::endl; + return 0; +} +``` + +这将输出: + +``` +Is func a function? true +Is vararg_func a function? true +``` + +你学会了吗,本节完~ \ No newline at end of file diff --git a/site_extras/cpp-things-std-ref-wrapper.md b/site_extras/cpp-things-std-ref-wrapper.md new file mode 100644 index 0000000..80183ea --- /dev/null +++ b/site_extras/cpp-things-std-ref-wrapper.md @@ -0,0 +1,146 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/std-ref-wrapper/) 的文章,由站点同步脚本导出。 + +# C++ ref 与 reference_wrapper + +## 深入理解 C++ 中的 std::cref、std::ref 和 std::reference_wrapper + +在 C++ 编程中,有时候我们需要在不进行拷贝的情况下传递引用,或者在需要引用的地方使用常量对象。为了解决这些问题,C++ 标准库提供了三个有用的工具:`std::cref`、`std::ref` 和 `std::reference_wrapper`。这篇文章将深入探讨这些工具的用途、区别以及实际应用。 + +此外,我们知道Rust语言中,经常实现了Unwrap方法,在C++中如何实现? + +这就参考Apache arrow的代码了,有这么一段: + +```cpp +std::optional> +TransportStatusDetail::Unwrap(const Status& status) { + std::shared_ptr detail = status.detail(); + if (!detail) return std::nullopt; + if (detail->type_id() != kTypeId) return std::nullopt; + return std::cref(arrow::internal::checked_cast(*detail)); +} + +``` + +随后我们便可以通过这样调用,提取出Status中实际的TransportStatusDetail。 + +```cpp +Status status(transportDetail); +auto result = Unwrap(status); +``` + +下面来从几个方面看看,最后给出整个Demo代码。 + +### 1. std::cref:创建常量引用 + +`std::cref` 是一个模板函数,用于创建对常量对象的引用。它返回一个 `std::reference_wrapper` 对象,可以在需要引用的地方使用。这在函数参数传递中特别有用,因为它允许我们在不进行拷贝的情况下传递常量对象,同时保持引用的语义。 + +**示例:** + +```cpp +#include +#include + +void printValue(const int& value) { + std::cout << "Value: " << value << std::endl; +} + +int main() { + int number = 42; + auto crefNumber = std::cref(number); + + printValue(crefNumber); // 使用常量引用传递参数 + + return 0; +} +``` + +### 2. std::ref:创建可修改的引用 + +与 `std::cref` 不同,`std::ref` 是一个模板函数,用于创建对可修改对象的引用。它返回一个 `std::reference_wrapper` 对象,允许我们在需要引用的地方使用,同时允许修改被引用的对象。 + +**示例:** + +```cpp +#include +#include + +void modifyValue(int& value) { + value *= 2; +} + +int main() { + int number = 42; + auto refNumber = std::ref(number); + + modifyValue(refNumber); // 使用可修改的引用作为参数 + + std::cout << "Modified Value: " << number << std::endl; + + return 0; +} +``` + +### 3. std::reference_wrapper:引用的包装器 + +`std::reference_wrapper` 是一个模板类,用于包装引用,使其能够在容器中存储或以引用的形式传递。它提供类似引用的语法,并且可以与标准容器一起使用,因为容器无法直接存储引用。 + +**示例:** + +```cpp +#include +#include +#include + +int main() { + int number1 = 42; + int number2 = 73; + + std::vector> numbers = {std::ref(number1), std::ref(number2)}; + + for (auto& num : numbers) { + num.get() += 10; // 修改原始对象的值 + } + + std::cout << "Number 1: " << number1 << std::endl; + std::cout << "Number 2: " << number2 << std::endl; + + return 0; +} +``` + +在这个示例中,`std::reference_wrapper` 允许我们将引用包装在容器中,然后通过 `get()` 方法来访问和修改原始对象的值。 + +## 4.Unwrap Demo + +这里给出具体的代码段,其完整代码参见知识星球内容。 + +```cpp +std::optional> Unwrap(const Status& status) { + std::shared_ptr detail = status.detail(); + if (!detail) return std::nullopt; + if (detail->type_id() != kTypeId) return std::nullopt; + return std::cref(static_cast(*detail)); +} + +int main() { + // 创建 TransportStatusDetail 对象 + std::shared_ptr transportDetail = std::make_shared(); + + // 创建 Status 对象并传入 TransportStatusDetail 对象 + Status status(transportDetail); + + auto result = Unwrap(status); + + if (result) { + const TransportStatusDetail& detail = result.value().get(); + // 使用 detail 进行操作 + std::cout << "TransportStatusDetail found." << std::endl; + } else { + std::cout << "No valid TransportStatusDetail found." << std::endl; + } + + return 0; +} +``` + +欢迎一起探讨C++那些事,更多项目/内容,欢迎加入知识星球。 \ No newline at end of file diff --git a/site_extras/cpp-things-tail-recursion-cpp.md b/site_extras/cpp-things-tail-recursion-cpp.md new file mode 100644 index 0000000..bcfd5fa --- /dev/null +++ b/site_extras/cpp-things-tail-recursion-cpp.md @@ -0,0 +1,51 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/tail-recursion-cpp/) 的文章,由站点同步脚本导出。 + +# C++ 尾递归 + +尾递归优化是一种特殊的优化技术,适用于递归函数,它可以极大地减少递归调用的栈空间占用。通过尾递归优化,编译器可以在执行尾递归时避免创建新的栈帧,直接复用当前的栈帧,从而降低栈溢出的风险,并提高性能。 + +[TOC] + +### 什么是尾递归? +递归调用在返回时,**没有其他操作**需要执行,这样的递归称为**尾递归**。在这种情况下,编译器可以用一个循环结构替代递归,避免多次函数调用的开销。 + +### 非尾递归的例子(斐波那契数列) +你提供的 `fibonacci_non_tail()` 函数是一个标准的非尾递归实现: + +```cpp +int fibonacci_non_tail(int n) { + if (n <= 1) { + return n; + } + return fibonacci_non_tail(n - 1) + fibonacci_non_tail(n - 2); +} +``` + +在这个实现中,递归调用 `fibonacci_non_tail(n - 1)` 和 `fibonacci_non_tail(n - 2)` 之后,还需要将它们的结果相加,因此这不是尾递归。每次调用都需要保存上下文信息,以便在递归返回后继续计算,这导致栈空间快速增长。 + +### 尾递归优化的示例 +`fibonacci_tail()` 使用了尾递归的方式: + +```cpp +int fibonacci_tail_helper(int n, int a, int b) { + if (n == 0) { + return a; + } + return fibonacci_tail_helper(n - 1, b, a + b); +} + +int fibonacci_tail(int n) { + return fibonacci_tail_helper(n, 0, 1); +} +``` + +这里的 `fibonacci_tail_helper()` 是尾递归。你可以看到,递归调用 `fibonacci_tail_helper(n - 1, b, a + b)` 后,函数不再需要做任何其他工作,直接返回递归调用的结果。因此,递归调用位于函数的“尾部”,没有额外的操作需要在递归调用后执行。 + +### 尾递归优化的优势 +- **减少栈空间使用**:由于不需要保存递归调用的上下文,编译器可以复用栈帧,避免栈空间膨胀。 +- **防止栈溢出**:尾递归优化允许递归深度达到非常大的值而不会发生栈溢出。 +- **更高效**:在支持尾递归优化的编译器中,尾递归的性能接近于迭代实现,消除了递归函数调用的开销。 + +### 性能测试 + +最后针对上面做了一个benchmark,发现尾递归优化在这个场景下非常有用,可以看到性能的差异很大! \ No newline at end of file diff --git a/site_extras/cpp-things-vector-resize-reverse.md b/site_extras/cpp-things-vector-resize-reverse.md new file mode 100644 index 0000000..28255d3 --- /dev/null +++ b/site_extras/cpp-things-vector-resize-reverse.md @@ -0,0 +1,45 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-things/vector-resize-reverse/) 的文章,由站点同步脚本导出。 + +# C++ vector resize 与 reverse + +在C++中,`resize`和`reserve`是用于容器(例如`std::vector`)的两个成员函数,用于管理vector的大小和内存分配。 + +例如: + +``` +std::vector v1; +v1.resize(1000); //allocation + instance creation +cout <<(v1.size() == 1000)<< endl; //prints 1 +cout <<(v1.capacity()==1000)<< endl; //prints 1 + +std::vector v2; +v2.reserve(1000); //only allocation +cout <<(v2.size() == 1000)<< endl; //prints 0 +cout <<(v2.capacity()==1000)<< endl; //prints 1 +``` + +`resize`函数用于更改容器的大小,可以增加或减少容器中元素的数量。当增加容器的大小时,新添加的元素将会进行初始化。当减少容器的大小时,超出新大小的元素将会被移除。 + +`reserve`函数用于预分配容器的内存空间,但不改变容器的大小。它可以用于提前分配足够的内存,以避免频繁的重新分配内存操作,从而提高性能。但是,调用`reserve`并不会改变容器的大小,只是为容器分配足够的内存空间。 + +需要注意的是,`resize`和`reserve`函数在容器类型上的可用性有所不同。`resize`函数对于大多数容器类型是可用的,而`reserve`函数只对具有动态大小的容器类型有效,例如`std::vector`和`std::deque`。对于固定大小的容器类型(例如`std::array`),调用`reserve`没有任何效果。 + +从源码层面上来看: + +``` +void resize(size_type new_size, value_type value = value_type()) { + if (new_size < size()) { + erase(begin() + new_size, end()); // 移除超出新大小的元素 + } else { + insert(end(), new_size - size(), value); // 添加新元素 + } +} +void reserve(size_type _n) { + if (__n > max_size()) + __throw_length_error(__N("vector::reserve")); + if (capacity() < __n) + _M_reallocate(__n); +} +``` + +> 本节完 \ No newline at end of file diff --git a/site_extras/cpp-tools-bazel-build-guide.md b/site_extras/cpp-tools-bazel-build-guide.md new file mode 100644 index 0000000..5c40c01 --- /dev/null +++ b/site_extras/cpp-tools-bazel-build-guide.md @@ -0,0 +1,61 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/bazel-build-guide/) 的文章,由站点同步脚本导出。 + +# Bazel 构建 C++ 项目 + +``` +gpadmin@gpadmin:~$ ./bazel-6.1.1-installer-linux-x86_64.sh --user +Bazel installer +--------------- + +Bazel is bundled with software licensed under the GPLv2 with Classpath exception. +You can find the sources next to the installer on our release page: + https://github.com/bazelbuild/bazel/releases + +# + +## Build information + - [Commit](https://github.com/bazelbuild/bazel/commit/1a90fe5) +Uncompressing......WARNING: Invoking Bazel in batch mode since it is not invoked from within a workspace (below a directory having a WORKSPACE file). +Extracting Bazel installation... +. + +Bazel is now installed! + +Make sure you have "/home/gpadmin/bin" in your path. + +For bash completion, add the following line to your : + source /home/gpadmin/.bazel/bin/bazel-complete.bash + +For fish shell completion, link this file into your +/home/gpadmin/.config/fish/completions/ directory: + ln -s /home/gpadmin/.bazel/bin/bazel.fish /home/gpadmin/.config/fish/completions/bazel.fish + +See http://bazel.build/docs/getting-started.html to start a new project! +``` + + + +默认安装到home/bin + + + +``` +gpadmin@gpadmin:~$ ls ~/bin/ +bazel +``` + + + + + +1. 在安装过程中,Bazel 可能会提示您添加环境变量。如果是这样,请根据提示编辑您的 `~/.bashrc` 文件并将以下行添加到文件末尾: + +``` +复制代码export PATH="$PATH:$HOME/bin" +``` + +1. 重新加载 `.bashrc` 文件以使更改生效: + +``` +复制代码source ~/.bashrc +``` \ No newline at end of file diff --git a/site_extras/cpp-tools-bazel-debugging.md b/site_extras/cpp-tools-bazel-debugging.md new file mode 100644 index 0000000..2ac9989 --- /dev/null +++ b/site_extras/cpp-tools-bazel-debugging.md @@ -0,0 +1,36 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/bazel-debugging/) 的文章,由站点同步脚本导出。 + +# Bazel 与 LLDB 联合调试 + +今天在调试时发现通过bazel编译出来的文件在mac上没法调试了,竟然全是汇编代码。 + +``` +* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 + frame #0: 0x0000000100000fd0 hello_world`main +hello_world`main: +-> 0x100000fd0 <+0>: pushq %rbp + 0x100000fd1 <+1>: movq %rsp, %rbp + 0x100000fd4 <+4>: subq $0x60, %rsp + 0x100000fd8 <+8>: movl $0x0, -0x4(%rbp) +(lldb) quit +``` + +如何解决呢? + +默认是standalone,我们需要disable,这里采用local,这意味着输出将与其依赖项链接,而不是采用默认的`standalone`策略。 + +```cpp +build --spawn_strategy=local +``` + +这样的话可以看到代码了。 + +```cpp +* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 + frame #0: 0x0000000100000fdf hello_world`main at hello_world.cc:14:21 + 11 + 12 int main() { + 13 // Create a thread pool +-> 14 auto threadPool = GetCpuThreadPool(); + 15 if (!threadPool) { +``` \ No newline at end of file diff --git a/site_extras/cpp-tools-cmake-essentials.md b/site_extras/cpp-tools-cmake-essentials.md new file mode 100644 index 0000000..0259a48 --- /dev/null +++ b/site_extras/cpp-tools-cmake-essentials.md @@ -0,0 +1,97 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/cmake-essentials/) 的文章,由站点同步脚本导出。 + +# CMake 入门与实战 + +## CMake常用必备一 + +[TOC] + +## 1.设置编译器flags + +例如:设置C++标准,显示所有警告。 + +```cpp +set(CMAKE_CXX_FLAGS "-std=c++0x -Wall") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -Wall") +add_compile_options("$<$,CXX>:-std=c++11>") +``` + +还可以设置一些优化flags + +官方资料:https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html + +例如: + +```cpp +-O0, -O1, -O2, -O3 +``` + +如果使用llvm,则可以参考这里: + +> https://clang.llvm.org/docs/DiagnosticsReference.html + +## 2.设置源文件属性 + +这是CMake的一个复杂属性,如果存在多个目标,可能需要更改一个目标的某些行为。例如,如果要使用C++11构建main.cpp,而只构建库,则可能希望将其构建为C++14。在这种情况下,您可以使用`set_source_files_properties`命令来配置特定源文件的属性,如下所示: + +``` +cmakeCopy code +set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/*.cpp PROPERTIES COMPILE_FLAGS "-std=c++11") +``` + +可以从以下手册中查看大量的属性,每个属性都可以用于满足您的特定需求。 + +- [CMake 属性](https://cmake.org/cmake/help/v3.3/manual/cmake-properties.7.html#source-file-properties) + +> https://cmake.org/cmake/help/v3.3/manual/cmake-properties.7.html#source-file-properties + +## 3.链接器标志 + +以下是GNU GCC链接器的链接器标志列表。 + +[GNU GCC 链接选项](https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html) + +> https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html + +GCC链接器默认情况下可以在环境变量中定义的目录(/usr/lib;/usr/local/lib)中搜索,然后默认链接标准库。 + +最常见的标志之一是`-l`,用于链接所需的库,如`-lzlib`、`-lboost`等。 + +还有其他附加标志,可帮助您更改可执行文件的链接选项行为。 + +以下是您可以添加链接器标志的变量: + +- `CMAKE_EXE_LINKER_FLAGS`:在创建可执行文件时由链接器使用的标志 +- `CMAKE_EXE_LINKER_FLAGS_RELEASE`:在创建发布版本可执行文件时由链接器使用的标志 +- `CMAKE_EXE_LINKER_FLAGS_DEBUG`:在创建调试版本可执行文件时由链接器使用的标志 +- `CMAKE_STATIC_LINKER_FLAGS`:在创建静态库(.a、.lib)时由链接器使用的标志 +- `CMAKE_SHARED_LINKER_FLAGS`:在创建共享库(.so、.dll)时由链接器使用的标志 +- `CMAKE_MODULE_LINKER_FLAGS`:在创建模块库时由链接器使用的标志 + +例如,您可以使用以下代码添加链接器标志: + +``` +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl") +``` + +了解如何使用这些标志可以帮助您更好地控制可执行文件的链接行为。 + +## 4.Debug与Release包 + +在CMakeLists.txt文件内部,可以访问CMAKE_BUILD_TYPE。你可以在CMakeLists.txt中轻松检查构建类型: + +```cpp +if(${CMAKE_BUILD_TYPE} MATCHES Debug) + message("Debug Build") +elseif(${CMAKE_BUILD_TYPE} MATCHES Release) + message("Release Build") +endif() +``` + +您还可以使用上一节中所示的配置变量,分别为不同的构建类型设置编译器和链接器标志: + +- CMAKE_EXE_LINKER_FLAGS_RELEASE:用于创建发布版本可执行文件的标志 +- CMAKE_EXE_LINKER_FLAGS_DEBUG:用于创建调试版本可执行文件的标志 +- CMAKE_CXX_FLAGS_RELEASE +- CMAKE_CXX_FLAGS_DEBUG \ No newline at end of file diff --git a/site_extras/cpp-tools-conan-package-manager.md b/site_extras/cpp-tools-conan-package-manager.md new file mode 100644 index 0000000..fa81dc6 --- /dev/null +++ b/site_extras/cpp-tools-conan-package-manager.md @@ -0,0 +1,210 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/conan-package-manager/) 的文章,由站点同步脚本导出。 + +# Conan C++ 包管理入门 + +## 1.Conan安装 + +``` +pip3 install conan +``` + +## 2.安装第三方库 + +安装第三方库时,我们通常需要知道安装的库版本,那么conan提供了相应的搜索命令,列出相应库的版本。 + +``` +conan search gtest --remote=conancenter +``` + +如果直接搜会报错,conancenter找不到,所以第一步设置remote。 + +``` +conan remote add conancenter https://center.conan.io +``` + +随后,我们再继续上面的搜索命令,发现报错: + +```cpp +ERROR: HTTPSConnectionPool(host='center.conan.io', port=443): Max retries exceeded with url: /v1/ping (Caused by SSLError(SSLError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:877)'),)) + +Unable to connect to conancenter=https://center.conan.io +1. Make sure the remote is reachable or, +2. Disable it by using conan remote disable, +Then try again. +``` + +此时,需要配置证书 + +``` +conan config install https://github.com/conan-io/conanclientcert.git +``` + +输出: + +```cpp +Trying to clone repo: https://github.com/conan-io/conanclientcert.git +Repo cloned! +Copying file LICENSE to /home/light/.conan/. +Copying file cacert.pem to /home/light/.conan/. +``` + +随后,继续使用`conan search`,我们找到: + +```cpp +[light@i-fu3d01tc build]$ conan search gtest --remote=conancenter +Existing package recipes: + +gtest/cci.20210126 +gtest/1.8.1 +gtest/1.10.0 +gtest/1.11.0 +gtest/1.12.1 +gtest/1.13.0 +gtest/1.14.0 +``` + +## 3.编写一个项目 + +接下来,使用conan编写一个项目。 + +项目背景:在开发阶段,我们需要进行单元测试,例如:除法需要测试除以0与非零等边界的断言,我们需要引入第三方库,这里用gtest。 + +1.编写代码 + +```cpp +#include +double divide(double a, double b) { return b == 0 ? 0 : a / b; } + +TEST(Div, NonZero) { + double expected = 1.5; + double actual = divide(3, 2); + ASSERT_EQ(expected, actual); +} + +TEST(Div, Zero) { + double expected = 0; + double actual = divide(3, 0); + ASSERT_EQ(expected, actual); +} + +``` + +2.编写conanfile.txt,指定一个gtest版本。 + +``` +[requires] +gtest/1.13.0 + +[generators] +cmake +``` + +3.添加CMakeLists.txt,内容很简单:其中感叹号部分需要注意。 + +```cpp +cmake_minimum_required(VERSION 3.16) +project(MultiplyTest LANGUAGES CXX) + +enable_testing() + +// !!! +find_package(GTest REQUIRED) + +add_executable(div_test div_test.cpp) + +// !!! +target_link_libraries(div_test + PRIVATE + GTest::GTest) + +add_test(div_gtest div_test) + +``` + +4.开始安装与编译 + +- 安装第三方库 + +``` +[gpadmin@i-fu3d01tc build]$ conan install ../ +Configuration: +[settings] +arch=x86_64 +arch_build=x86_64 +build_type=Release +compiler=gcc +compiler.libcxx=libstdc++11 +compiler.version=10 +os=Linux +os_build=Linux +[options] +[build_requires] +[env] + +gtest/1.13.0: Not found in local cache, looking in remotes... +gtest/1.13.0: Trying with 'conan-public'... +gtest/1.13.0: Trying with 'conancenter'... +Downloading conanmanifest.txt completed [0.10k] +Downloading conanfile.py completed [8.27k] +Downloading conan_export.tgz completed [0.32k] +Decompressing conan_export.tgz completed [0.00k] +gtest/1.13.0: Downloaded recipe revision 8a0bc5b3e159ed45de97260c2bff65b5 +conanfile.txt: Installing package +Requirements + gtest/1.13.0 from 'conancenter' - Downloaded +Packages + gtest/1.13.0:e019a06362b932ca5d1b082b6c112aa150c88de4 - Download + +Installing (downloading, building) binaries... +gtest/1.13.0: Retrieving package e019a06362b932ca5d1b082b6c112aa150c88de4 from remote 'conancenter' +Downloading conanmanifest.txt completed [3.07k] +Downloading conaninfo.txt completed [0.60k] +Downloading conan_package.tgz completed [487.88k] +Decompressing conan_package.tgz completed [0.00k] +gtest/1.13.0: Package installed e019a06362b932ca5d1b082b6c112aa150c88de4 +gtest/1.13.0: Downloaded package revision 81bd558bf2388a3aee4ee13b8a902a45 +conanfile.txt: Generator txt created conanbuildinfo.txt +conanfile.txt: Generator cmake created conanbuildinfo.cmake +conanfile.txt: Aggregating env generators +conanfile.txt: Generated conaninfo.txt +conanfile.txt: Generated graphinfo +``` + +- 编译 + +```cpp +[light@i-fu3d01tc build]$ cmake .. +-- The CXX compiler identification is GNU 10.2.1 +-- Detecting CXX compiler ABI info +-- Detecting CXX compiler ABI info - done +-- Check for working CXX compiler: /opt/rh/devtoolset-10/root/usr/bin/c++ - skipped +-- Detecting CXX compile features +-- Detecting CXX compile features - done +-- Found GTest: /usr/lib64/libgtest.so +-- Configuring done +-- Generating done +-- Build files have been written to: /home/light/cmake-google-tests/conan/build +[light@i-fu3d01tc build]$ make +Scanning dependencies of target div_test +[ 50%] Building CXX object CMakeFiles/div_test.dir/div_test.cpp.o +[100%] Linking CXX executable div_test +[100%] Built target div_test +``` + +- 运行 + +``` +[light@i-fu3d01tc build]$ ./div_test +[==========] Running 2 tests from 1 test case. +[----------] Global test environment set-up. +[----------] 2 tests from Div +[ RUN ] Div.NonZero +[ OK ] Div.NonZero (0 ms) +[ RUN ] Div.Zero +[ OK ] Div.Zero (0 ms) +[----------] 2 tests from Div (0 ms total) + +[----------] Global test environment tear-down +[==========] 2 tests from 1 test case ran. (0 ms total) +[ PASSED ] 2 tests. +``` \ No newline at end of file diff --git a/site_extras/cpp-tools-gcc-flags-guide.md b/site_extras/cpp-tools-gcc-flags-guide.md new file mode 100644 index 0000000..40f7ef9 --- /dev/null +++ b/site_extras/cpp-tools-gcc-flags-guide.md @@ -0,0 +1,81 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/gcc-flags-guide/) 的文章,由站点同步脚本导出。 + +# GCC 编译选项详解 + +大家经常在编译代码时通常会使用一些gcc flags,今天这篇文章来梳理一下一些比较重要的gcc flag。 + +## 1.优化flag + +优化标志用于提高编译代码的性能。这些标志可以加速编译过程并生成高效的程序。 + +一些常用的优化标志包括: + +`-O1`、`-O2`、`-O3`、`-Os`:这些标志控制应用的优化级别。 + +`-O1`应用基本优化,而`-O2`和`-O3`提供更高级的优化。-Os优化代码大小。 + + `-finline-functions`、`-funroll-loops`:这些标志分别控制函数内联和循环展开优化,旨在消除函数调用开销并减少循环迭代。 + +## 2.调试Flag + +调试标志有助于提供额外信息,使调试工具能够跟踪和分析代码。 + +一些常用的调试标志包括: + +`-g`:该标志在编译后的可执行文件中包含调试符号。 + + `-ggdb`:该标志使GCC以适用于GNU调试器(GDB)的格式生成调试信息。 + +## 3.警告Flag + +警告标志有助于识别潜在问题、编码错误或代码中存在问题的实践。通过启用警告标志,编译器会对这些情况提供警告,使开发人员能够提高代码质量。 + +一些常用的警告标志包括: + + `-Wall`:该标志启用一套全面的警告,涵盖各种潜在问题。 + + `-Werror`:该标志将警告视为错误,使编译器在遇到警告时停止编译过程。 + +## 4.预处理器Flag + +预处理器标志负责处理源代码中以#开头的指令。这些标志定义宏并控制编译。 + +一些常用的预处理器标志包括: + + `-D`:该标志用于在编译过程中定义宏。例如,`-DDEBUG`可用于定义DEBUG宏,根据其是否存在启用条件编译。 + + `-I`:该标志用于指定头文件位于的额外目录。它帮助预处理器在编译代码时找到必要的头文件。 + +## 5.链接器Flag + +链接器标志控制链接过程,该过程在编译后进行,将目标文件和库组合以生成最终的可执行文件。 + +一些常用的链接器标志包括: + + `-l`:该标志用于链接外部库。例如:`-lpthread`链接POSIX线程库。 + + `-L`:该标志用于指定库位于的额外目录。它帮助链接器在链接过程中找到所需的库。 + +## 6.平台特定flag + + GCC提供了针对特定平台的标志,允许开发人员针对特定架构或启用特定平台功能。在为特定硬件或操作系统开发代码时,这些标志非常有用。 + +平台特定标志包括: + + `-march`:该标志指定目标架构。例如,`-march=native`指示GCC优化为主机机器的架构。 + + `-m32`和`-m64`:这些标志指定是为32位还是64位目标进行编译。 + +## 7.其他Flag + + 除了前面提到的类别,还有一些其他标志。 + +一些常用的杂项标志包括: + + `-std`:该标志指定要使用的C语言标准版本。例如,`-std=c99`选择C99标准。 `-pedantic`:该标志启用对所选择的C语言标准的严格遵守,对非标准或可疑代码结构提供额外的警告。 `-Wextra`:该标志启用超过-Wall启用的额外警告,提供更全面的代码分析。 + +需要注意的是,标志的可用性和功能可能会因GCC的不同版本和平台而有所变化。因此,建议查阅GCC文档和相关资源,获取有关特定标志及其用法的详细信息。 + +完整文档地址: + +> https://gcc.gnu.org/onlinedocs/gcc/ \ No newline at end of file diff --git a/site_extras/cpp-tools-gdb-debugging-guide.md b/site_extras/cpp-tools-gdb-debugging-guide.md new file mode 100644 index 0000000..9f747b7 --- /dev/null +++ b/site_extras/cpp-tools-gdb-debugging-guide.md @@ -0,0 +1,34 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/gdb-debugging-guide/) 的文章,由站点同步脚本导出。 + +# GDB 调试 C++ 程序 + +当使用GNU Debugger(GDB)调试C代码时,你会发现它是UNIX系统上最受欢迎的调试器,用于调试C和C++程序。下面是一个简明易懂的使用GDB调试C代码的解释: + +首先,在命令行中切换到包含代码的当前文件夹,并运行以下命令: + +gcc -g -Wall code.c -o output + +其中,`-g`选项对于使用GDB工具是必要的。`-Wall`选项用于显示警告信息。这将创建一个名为"output"的可执行文件。 + +然后,运行以下命令打开GDB对话框并从output文件中获取符号信息(或简单地说,设置整个环境以调试code.c): + +gdb output + +使用以下命令可以帮助你进行调试: + +- `list`:列出源代码。 +- `b x`:在x处设置断点。x可以是模块的名称或行号。例如,x可以是main等函数名称。 +- `r`:执行程序(直到断点),并提供结果。即,在断点处运行代码并生成结果。[如果有参数,则为r 参数1 参数2 参数3 ...] +- `info breakpoints x`:给出断点x的详细信息。 +- `info breakpoints`:给出所有断点的详细信息。 +- `delete x`:删除断点x。 +- `disable x`和`enable x`:禁用和启用断点x。 +- `print p`:打印变量p的值。 +- `display p`:在每次运行后显示变量p的值(自动化整个过程)。 +- `c`:继续执行程序(跳过for循环等迭代)。使用`c-iter1`跳过第一次迭代,再次输入`c`跳过第二次迭代,依此类推。 +- 你还可以使用额外的条件,例如`b x if (y > m)`,意思是如果y的值大于m,则在第x行处设置断点。 +- GDB还可以在多个文件中设置断点。例如,如果program.c中调用了function.c中的一个函数,你可以使用`b function.c:x`在function.c的第x行设置断点。 +- `bt`:用于回溯代码执行的路径。`bt`会给出一个索引列表,显示代码经过的进程。例如:#0 进程0的描述,#1 进程1的描述,依此类推。其中,0是最近的进程。 +- 输入`f x`将跳转到进程x(`f 0`将跳转到进程0),并给出对应函数执行时的变量值。 +- `s`:用于进入函数并获取更多详细信息。 +- `next`:用于逐行执行代码。 \ No newline at end of file diff --git a/site_extras/cpp-tools-gdb-python-enhancement.md b/site_extras/cpp-tools-gdb-python-enhancement.md new file mode 100644 index 0000000..c84eb30 --- /dev/null +++ b/site_extras/cpp-tools-gdb-python-enhancement.md @@ -0,0 +1,122 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/gdb-python-enhancement/) 的文章,由站点同步脚本导出。 + +# GDB Python 扩展调试 + +## 0.导语 + +调试是软件开发过程中不可或缺的一环,而GDB(GNU调试器)作为一款功能强大的调试工具,在开发者中得到广泛应用。除了传统的命令行调试功能外,GDB还提供了Python的GDB库,允许开发者使用Python语言扩展和增强调试的能力。本文将介绍如何在GDB中使用Python的GDB库,展示其强大的调试功能和用例。 + +开始之前,确保已经正确安装Python和GDB,并且Python版本与GDB兼容。然后,按照以下步骤安装Python的GDB库: + +a. 在命令行中执行`gdb`打开GDB。 + +b. 输入`python`命令,进入GDB的Python解释器。 + +c. 执行`import gdb`,如果没有报错,说明Python的GDB库已经正确安装。 + +```gdb +(gdb) python +>import gdb +>end +``` + +gdb调试过程中我们通常想查看一些复杂结构变量,例如PostgreSQL中的List结构,啥都能往里面放,里面具体是啥真不好直观打印出来,此时在我们日常开发中就有了python脚本,下面示例: + +```gdb +(gdb) tl +TargetEntry [resno=1 resname="sum"] + Aggref [aggfnoid=2107 aggtype=1700 aggtranstype=2281 aggstar=false aggvariadic=false aggkind=110 'n' agglevelsup=0 aggsplit=AGGSPLIT_FINAL_DESERIAL aggno=0 aggtransno=0 agg_expr_id=0] +``` + +## 1.Hello python gdb + +我们可以在gdb中执行一些python操作,例如:print,如下所示: + +``` +scssCopy code +(gdb) python +> print(1+2) +> end +3 +(gdb) end +This command cannot be used at the top level. +``` + +在这个示例中,我们进入GDB的Python解释器,并执行了一个简单的Python语句`print(1+2)`,结果为3。 + +另一个例子是我们可以直接输入python xxxx来运行。 + +``` +scssCopy code +(gdb) python f = 2 +(gdb) python print(f) +2 +``` + +在这个示例中,我们在GDB中使用Python语句定义了一个变量`f`,并打印了它的值,结果为2。 + +## 2.实战:如何编写? + +核心点在于我们如何编写这些脚本?通过这个脚本来方便打印出自己代码,下面是实战环节,关于本节的相关代码与资料将会更新在星球中,欢迎关注: + + + +```cpp +struct Point { + int x; + int y; +}; +``` + +现在有一个Point结构体,我想通过脚本打印出来,如何实现呢? + +逻辑非常简单,读取结构体,然后print即可。 + +```python +import gdb + +def print_point(point): + print(f"Point: x={point['x']}, y={point['y']}") + +def print_person(person): + age = person['age'] + print(f"Person: age={age}") + +class PrintCppStruct(gdb.Command): + def __init__(self): + super(PrintCppStruct, self).__init__("print_cpp_struct", gdb.COMMAND_USER) + + def invoke(self, arg, from_tty): + try: + cl = gdb.parse_and_eval(arg) + if cl.type.code == gdb.TYPE_CODE_STRUCT and str(cl.type) == 'Point': + print_point(cl) + elif cl.type.code == gdb.TYPE_CODE_STRUCT and str(cl.type) == 'Person': + print_person(cl) + else: + print("Unknown C++ structure type.") + except gdb.error as e: + print(f"Error: {e}") + +PrintCppStruct() + +``` + +如何使用? + +只需要source即可: + +``` +(gdb) source print.py +``` + +在这个示例中,我们通过`source`命令在GDB中加载了一个名为`print.py`的Python脚本。 + +```cpp +(gdb) print_cpp_struct myPoint +Point: x=10, y=20 +``` + +在这个示例中,我们在GDB中使用了一个自定义的命令`print_cpp_struct`,它通过Python的GDB库打印了一个名为`myPoint`的C++结构体的值。 + +通过在GDB中使用Python的GDB库,开发者可以利用Python的强大功能扩展和增强调试的能力。它提供了丰富的API和灵活的脚本编写能力,使得调试过程更加高效和便捷。无论是自动化调试任务、数据分析和可视化,还是动态修改程序行为,Python的GDB库都为开发者提供了丰富的选择和灵活性。希望本文能够帮助您更好地理解和应用Python的GDB库,提升调试技能和开发效率。 \ No newline at end of file diff --git a/site_extras/cpp-tools-lldb-debugging.md b/site_extras/cpp-tools-lldb-debugging.md new file mode 100644 index 0000000..30057a8 --- /dev/null +++ b/site_extras/cpp-tools-lldb-debugging.md @@ -0,0 +1,224 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/lldb-debugging/) 的文章,由站点同步脚本导出。 + +# LLDB 调试 C++ 程序 + +[TOC] + +本文将介绍如何使用 LLDB 调试器调试一个简单的 C++ 程序。我们的示例代码命名为 `test.cpp`。 + +```cpp +#include +#include + +struct MyStruct { + int myInt; + std::string myString; +}; + +int main() { + MyStruct myStruct; + myStruct.myInt = 42; + myStruct.myString = "Hello, world!"; + + int sum = 0; + for (int i = 1; i <= 5; ++i) { + sum += i; + } + + return 0; +} +``` + +## 编译 + +首先,我们需要使用 `-g` 标志编译代码以启用额外的调试信息。 + +```sh +clang++ -g test.cpp -o test +``` + +## 开始调试 + +接着,我们附加调试器。 + +```sh +lldb test +``` + +## 断点 + +我们将断点设置在 `main()` 函数。 + +```sh +(lldb) b main +``` + +输出: + +Breakpoint 1: where = test`main + 19 at test.cpp:10:14, address = 0x0000000100003be3 + +## 运行 + +然后,我们启动进程。 + +```sh +(lldb) run +``` + +输出: + +```cpp +Process 11333 launched: '/Users/light/test' (x86_64) +Process 11333 stopped +* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 + frame #0: 0x0000000100003be3 test`main at test.cpp:10:14 + 7 }; + 8 + 9 int main() { +-> 10 MyStruct myStruct; + 11 myStruct.myInt = 42; + 12 myStruct.myString = "Hello, world!"; + 13 +``` + +## 查看断点 + +现在,我们已经停在了 `main()` 函数的开头。我们可以设置在特定行上的断点。 + +```sh +(lldb) b 16 +``` + +输出:Breakpoint 2: where = test`main + 76 at test.cpp:16:16, address = 0x0000000100003c1c + +要列出当前设置的断点,请使用以下命令。 + +```sh +(lldb) breakpoint list +``` + +输出: + +``` +(lldb) breakpoint list +Current breakpoints: +1: name = 'main', locations = 1, resolved = 1, hit count = 1 + 1.1: where = test`main + 19 at test.cpp:10:14, address = 0x0000000100003be3, resolved, hit count = 1 + +2: file = '/Users/zhangxing/test.cpp', line = 16, exact_match = 0, locations = 1, resolved = 1, hit count = 0 + 2.1: where = test`main + 76 at test.cpp:16:16, address = 0x0000000100003c1c, resolved, hit count = 0 +``` + +## 程序继续运行 + +我们可以继续执行程序直到下一个断点。 + +```sh +(lldb) c +``` + +输出: + +``` +Process 11823 resuming +Process 11823 stopped +* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1 + frame #0: 0x0000000100003c1c test`main at test.cpp:16:16 + 13 + 14 int sum = 0; + 15 for (int i = 1; i <= 5; ++i) { +-> 16 sum += i; + 17 } + 18 + 19 return 0; +``` + +## 打印变量 + +要获取当前局部变量的值,可以使用以下命令: + +```sh +(lldb) p myStruct +``` + +输出: + +``` +(MyStruct) $0 = (myInt = 42, myString = "Hello, world!") +``` + +## 观察点 + +要监视特定变量的值更改,我们可以使用监视点。 + +```sh +(lldb) watch set variable sum +``` + +输出: + +```cpp +Watchpoint created: Watchpoint 1: addr = 0x7ffeefbff1e8 size = 4 state = enabled type = w + declare @ '/Users/zhangxing/test.cpp:14' + watchpoint spec = 'sum' + new value: 0 +``` + +当next后,会看到Watchpoint的变化。 + +```cpp +(lldb) n + +Watchpoint 1 hit: +old value: 3 +new value: 6 +Process 11823 stopped +* thread #1, queue = 'com.apple.main-thread', stop reason = watchpoint 1 + frame #0: 0x0000000100003c25 test`main at test.cpp:15:29 + 12 myStruct.myString = "Hello, world!"; + 13 + 14 int sum = 0; +-> 15 for (int i = 1; i <= 5; ++i) { + 16 sum += i; + 17 } + 18 + +``` + +如果想要根据条件停止执行,可以设置停止条件。 + +```sh +(lldb) watch modify -c '(sum > 5)' +``` + +输出: + +```cpp +Watchpoint 1 hit: +new value: 6 +Process 12848 stopped +* thread #1, queue = 'com.apple.main-thread', stop reason = watchpoint 1 + frame #0: 0x0000000100003c25 test`main at test.cpp:15:29 + 12 myStruct.myString = "Hello, world!"; + 13 + 14 int sum = 0; +-> 15 for (int i = 1; i <= 5; ++i) { + 16 sum += i; + 17 } + 18 +``` + +完成调试后,可以继续执行程序。 + +```sh +(lldb) c +``` + +如需了解更多信息,请参阅 LLDB 文档: + +- [LLDB 和 GDB 对照指南](https://lldb.llvm.org/lldb-gdb.html) +- [LLDB 教程](https://lldb.llvm.org/tutorial.html) + +--- + +通过这些步骤,您可以轻松地使用 LLDB 调试您的 C++ 程序,查找错误并解决问题。 \ No newline at end of file diff --git a/site_extras/cpp-tools-memory-leak-debug-session.md b/site_extras/cpp-tools-memory-leak-debug-session.md new file mode 100644 index 0000000..1d2eed8 --- /dev/null +++ b/site_extras/cpp-tools-memory-leak-debug-session.md @@ -0,0 +1,50 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/memory-leak-debug-session/) 的文章,由站点同步脚本导出。 + +# C++ 内存泄漏排查实战 + +``` +regression=# SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_e t1, prt2_e t2 WHERE (t1.a + t1.b)/2 = (t2.b + t2.a)/2 AND t1.c = 0 ORDER BY t1.a, t2.b; +WARNING: problem in alloc set ExecutorState: bad single-chunk 0x7cfdd48 in block 0x7cfc580 (seg1 slice1 127.0.1.1:7003 pid=4295) +WARNING: problem in alloc set ExecutorState: found inconsistent memory block 0x7cfc580 (seg1 slice1 127.0.1.1:7003 pid=4295) +WARNING: problem in alloc set ExecutorState: bad single-chunk 0x7cfdd48 in block 0x7cfc580 (seg1 slice1 127.0.1.1:7003 pid=4295) +WARNING: problem in alloc set ExecutorState: found inconsistent memory block 0x7cfc580 (seg1 slice1 127.0.1.1:7003 pid=4295) +WARNING: problem in alloc set ExecutorState: bad single-chunk 0x87f85c8 in block 0x87f6e00 (seg0 slice1 127.0.1.1:7002 pid=4294) +WARNING: problem in alloc set ExecutorState: found inconsistent memory block 0x87f6e00 (seg0 slice1 127.0.1.1:7002 pid=4294) +WARNING: problem in alloc set ExecutorState: bad single-chunk 0x7d3c5c8 in block 0x7d3ae00 (seg2 slice1 127.0.1.1:7004 pid=4296) +WARNING: problem in alloc set ExecutorState: found inconsistent memory block 0x7d3ae00 (seg2 slice1 127.0.1.1:7004 pid=4296) +WARNING: problem in alloc set ExecutorState: bad single-chunk 0x87f85c8 in block 0x87f6e00 (seg0 slice1 127.0.1.1:7002 pid=4294) +WARNING: problem in alloc set ExecutorState: bad single-chunk 0x7d3c5c8 in block 0x7d3ae00 (seg2 slice1 127.0.1.1:7004 pid=4296) +WARNING: problem in alloc set ExecutorState: found inconsistent memory block 0x87f6e00 (seg0 slice1 127.0.1.1:7002 pid=4294) +WARNING: problem in alloc set ExecutorState: found inconsistent memory block 0x7d3ae00 (seg2 slice1 127.0.1.1:7004 pid=4296) +``` + + + +调试: + +``` +(gdb) b aset.c:1109 if chunk=0x7cfdd48 +Breakpoint 1 at 0x105d420: file aset.c, line 1109. +(gdb) c +Continuing. + +Thread 1 "postgres" hit Breakpoint 1, AllocSetAlloc (context=0x787ac50, size=4096) at aset.c:1109 +1109 block->freeptr += (chunk_size + ALLOC_CHUNKHDRSZ); +(gdb) p chunk +$1 = (AllocChunk) 0x7cfdd48 +(gdb) p chunk->size +$2 = 9187201950435737471 +(gdb) p &chunk->size +$3 = (Size *) 0x7cfdd48 +(gdb) watch (*((Size *) 0x7cfdd48)) +Hardware watchpoint 2: (*((Size *) 0x7cfdd48)) +(gdb) i b +Num Type Disp Enb Address What +1 breakpoint keep y 0x000000000105d420 in AllocSetAlloc at aset.c:1109 + stop only if chunk=0x7cfdd48 + breakpoint already hit 1 time +2 hw watchpoint keep y (*((Size *) 0x7cfdd48)) +(gdb) d 1 +(gdb) c + +``` \ No newline at end of file diff --git a/site_extras/cpp-tools-meson-build.md b/site_extras/cpp-tools-meson-build.md new file mode 100644 index 0000000..3429cb3 --- /dev/null +++ b/site_extras/cpp-tools-meson-build.md @@ -0,0 +1,384 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/meson-build/) 的文章,由站点同步脚本导出。 + +# Meson 构建 C++ 项目 + +最近在使用一些开源项目的时候,发现都采用了meson+ninja的构建方式,摒弃了CMake,Makefile的方式,本文将会引入Meson,从入门安装到完整编写一个meson项目。 + +下面,我们来进入正文。 + +## 1.Meson安装 + +> https://github.com/mesonbuild/meson + +Meson 可以通过 PyPi 获得,因此可以使用 pip3 install meson 进行安装。确保在不同系统中使用正确的 pip 命令,务必使用 Python 3 版本的 pip。 + +如果希望,也可以使用标准的 Python 命令在本地安装: + +```bash +python3 -m pip install meson +``` + +对于使用 Ninja 进行构建的情况,可以直接从 Ninja GitHub 发布页面或通过 PyPi 下载 Ninja: + +```bash +python3 -m pip install ninja +``` + +有关安装 Meson build 的更多信息,请阅读下方Meson官方网站。 + +> https://mesonbuild.com/Getting-meson.html + +## 2.Meson Vs Cmake + +既然大家都用CMake,为何还要用Meson呢,他们各自优劣是什么呢? + +在性能方面,Meson 通常以其快速的构建速度而闻名。Meson 的设计旨在优化构建速度,并且在增量构建方面表现特别出色。Meson 使用 Ninja 构建系统作为默认后端,Ninja 本身也以快速和高效而著称。 + +相比之下,CMake 的构建速度可能会受到项目规模的影响,尤其是在较大的项目中。CMake 使用各种生成器(如Makefile),而不同生成器的性能也可能有所不同。 + +当然也可以使用CMake + Ninja去提高编译速度。 + +从开源项目角度来看Meson,其使用场景也是非常的多,例如:[PostgreSQL](https://www.postgresql.org/)、[QEMU](https://qemu.org/)、[GNOME Software](https://gitlab.gnome.org/GNOME/gnome-software)、[GTK+](https://gitlab.gnome.org/GNOME/gtk),等等 + +> https://mesonbuild.com/Users.html + +### 2.1 Meson的优势与劣势 + +1.优势 + +- 基于Python,使用简单 +- 支持交叉编译 +- 所有对象都是强类型的:由于对象是诸如 'dependency'、'include directory' 等实体,因此很难出现字符串替换错误。 +- 非常方便的引用第三发库 +- **非常非常快,特别是在增量构建时。** +- 文档比cmake好n倍。 + +> http://mesonbuild.com + +2.劣势 + +- 可能不如CMake成熟 +- 有Python3 + Ninja的依赖 +- 项目生成器,IDE支持Meson不如CMake + - vscode支持 + - https://github.com/mesonbuild/vscode-meson +- 可用模块有限 + - 需要自己将非meson项目提交到wrapdb + - https://mesonbuild.com/Wrapdb-projects.html + +### 2.2 CMake的优势与劣势 + +1.优势 + +- IDE支持友好,团队协作比较友 +- 成熟,几乎成为C++项目的标准 + +2.缺点 + +- 交叉编译困难 +- 编译速度相对比较慢 + +## 3.Meson WrapDB + +由于googletest不是meson项目,那如何使用meson去依赖它呢? + +同理,grpc、abseil-cpp使用方式类似。 + +这里就要先引入WrapDB,Meson WrapDB 是 Meson 构建系统的一个组件,用于管理和提供第三方项目的构建描述文件,通常称为 "wrap" 文件。这些 wrap 文件包含了配置和构建第三方项目所需的信息,以便能够轻松地集成到使用 Meson 构建系统的其他项目中。 + +可以通过search命令或者下方网站查询第三方库。 + +> https://mesonbuild.com/Wrapdb-projects.html#meson-wrapdb-packages + +我们以gtest使用为例: + +- 搜索gtest + +``` +meson wrap search gtest +``` + +如果该命令返回包装的项目名称,那么便可以进行下一步了: + +- 在自己的项目创建subprojects目录,通过install安装gtest + +``` +mkdir -p subprojects +meson wrap install gtest +``` + +此时你的项目所依赖的第三方库均已在subprojects目录安装好了。 + +```cpp +➜ gtest_div tree -L 1 subprojects +subprojects +├── gtest.wrap +``` + +此时会生成gtest.wrap,里面的内容会包含下面的内容。 + +注意:此文件是上方install之后产生的文件,建议不要修改。 + +```cpp +[wrap-file] +directory = googletest-1.14.0 +source_url = https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz +source_filename = gtest-1.14.0.tar.gz +source_hash = 8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7 +patch_filename = gtest_1.14.0-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.14.0-1/get_patch +patch_hash = 2e693c7d3f9370a7aa6dac802bada0874d3198ad4cfdf75647b818f691182b50 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.14.0-1/gtest-1.14.0.tar.gz +wrapdb_version = 1.14.0-1 + +[provide] +gtest = gtest_dep +gtest_main = gtest_main_dep +gmock = gmock_dep +gmock_main = gmock_main_dep + +``` + +## 4.如何编写一个meson项目? + +接下来,来到实践环节,如何编写一个meson项目呢? + +下面以a/b除法测试为例,使用gtest进行单元测试的一个小项目。 + +### 4.1 编写代码 + +``` +double divide(double a, double b); +``` + +.h与.cc放到src目录: + +```cpp +src +├── division.cc +└── division.h +``` + +随后同级目录创建test目录。 + +```cpp +test +└── division_test.cc +``` + +### 4.2 meson项目管理 + +在项目根路径添加meson.build文件,内容如下: + +```python +project('gtest_div', 'cpp') +gtest_proj = subproject('gtest') +gtest_dep = gtest_proj.get_variable('gtest_main_dep') +div_src = files('src/division.cc') +tests_src = files('test/division_test.cc') +e = executable( + 'division_test', + tests_src + div_src, + dependencies : [ gtest_dep], + override_options : ['cpp_std=c++14'] +) +# https://mesonbuild.com/Unit-tests.html#testing-tool +test('this is a division test', e) +``` + +可以看到这个内容跟python的语法基本一致,熟悉python的看这个代码就非常容易了,这里着重讲一下上面比较重要的部分。 + +- 引入第三方库 + +例如:你引入的第三方库的项目叫做gtest,如下: + +``` +project('gtest', 'cpp', + version : '1.14.0', + default_options : ['cpp_std=c++14'], + license : 'bsd') +``` + +那么便可以通过subproject获取到这个project,然后通过`gtest_proj.get_variable`便可以获取到gtest的依赖信息,例如:googletest里面是这样声明的: + +```c +gtest_main_dep = declare_dependency( + include_directories : gtest_incdir, + sources : [gtest_libsources, gtest_mainsources], + dependencies : dependency('threads') +) +``` + +- executable + +executable跟cmake里面的add_executable非常类似,生成可执行文件division_test。 + +- test + +单元测试语法: + +``` +e = executable('prog', 'testprog.c') +test('name of test', e) +``` + +可以使用`meson test`或者`ninja test`,运行完之后会产生测试日志,位于: + +``` +build/meson-logs/testlog.txt +``` + +### 4.3 构建与运行Meson项目 + +经过上面的操作,一个meson项目准备就绪。接下来,我们来构建与运行这个项目。 + +#### 4.3.1 配置构建目录 + +此时类似于cmake的`mkdir build && cmake ..`。 + +```cpp +meson setup build +``` + +运行如下: + +``` +➜ gtest_div meson setup build +The Meson build system +Version: 1.2.3 +Source dir: /Users/light/work/practice/gtest_div +Build dir: /Users/light/work/practice/gtest_div/build +Build type: native build +Project name: gtest_div +Project version: undefined +C++ compiler for the host machine: c++ (clang 12.0.5 "Apple clang version 12.0.5 (clang-1205.0.22.11)") +C++ linker for the host machine: c++ ld64 650.9 +Host machine cpu family: x86_64 +Host machine cpu: x86_64 +Downloading gtest source from https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz +Download size: 867764 +Downloading: .......... +Downloading gtest patch from https://wrapdb.mesonbuild.com/v2/gtest_1.14.0-1/get_patch +Download size: 2440 +Downloading: .......... + +Executing subproject gtest + +gtest| Project name: gtest +gtest| Project version: 1.14.0 +gtest| C++ compiler for the host machine: c++ (clang 12.0.5 "Apple clang version 12.0.5 (clang-1205.0.22.11)") +gtest| C++ linker for the host machine: c++ ld64 650.9 +gtest| Run-time dependency threads found: YES +gtest| Dependency threads found: YES unknown (cached) +gtest| Dependency threads found: YES unknown (cached) +gtest| Dependency threads found: YES unknown (cached) +gtest| Build targets in project: 0 +gtest| Subproject gtest finished. + +Build targets in project: 1 + +gtest_div undefined + + Subprojects + gtest: YES + +Found ninja-1.11.1 at /usr/local/bin/ninja +``` + +此时我们可以在subprojects看到缓存的gtest项目。 + +``` +subprojects +├── googletest-1.14.0 # here +├── gtest.wrap +└── packagecache # here +``` + +#### 4.3.2 编译 + +我们可以使用下面两个命令,第一种: + +``` +meson compile -C build/ +``` + +运行日志: + +``` +➜ gtest_div meson compile -C build/ +INFO: autodetecting backend as ninja +INFO: calculating backend command to run: /usr/local/bin/ninja -C /Users/light/work/practice/gtest_div/build +ninja: Entering directory `/Users/light/work/practice/gtest_div/build' +[5/5] Linking target division_test +``` + +第二种: + +``` +➜ gtest_div ninja -C build +ninja: Entering directory `build' +[5/5] Linking target division_test +``` + +这两种命令本质是一样,都会用ninja来编译,当然也可以不用`-C`指定目录,那么此时需要`cd build && ninja`或者`cd build && meson compile`。 + +#### 4.3.3 运行与测试 + +此时会生成上division_test可执行程序,运行如下: + +``` +➜ gtest_div ./build/division_test +Running main() from ../subprojects/googletest-1.14.0/googletest/src/gtest_main.cc +[==========] Running 2 tests from 1 test suite. +[----------] Global test environment set-up. +[----------] 2 tests from DivisionTest +[ RUN ] DivisionTest.Divide +[ OK ] DivisionTest.Divide (0 ms) +[ RUN ] DivisionTest.DivideByZero +[ OK ] DivisionTest.DivideByZero (0 ms) +[----------] 2 tests from DivisionTest (0 ms total) + +[----------] Global test environment tear-down +[==========] 2 tests from 1 test suite ran. (0 ms total) +[ PASSED ] 2 tests. +``` + +由于在meson.build中添加单元测试test,所以我们可以使用 + +``` +meson test +``` + +或者: + +``` +ninja test +``` + +如果指定目录: + +``` +meson test -C build/ + +or + +ninja test -C build/ +``` + +运行日志: + +``` +➜ gtest_div meson test -C build +ninja: Entering directory `/Users/light/work/practice/gtest_div/build' +ninja: no work to do. +1/1 this is a division test OK 0.01s + +Ok: 1 +Expected Fail: 0 +Fail: 0 +Unexpected Pass: 0 +Skipped: 0 +Timeout: 0 + +Full log written to /Users/light/work/practice/gtest_div/build/meson-logs/testlog.txt +``` \ No newline at end of file diff --git a/site_extras/cpp-tools-stl-debug-mode.md b/site_extras/cpp-tools-stl-debug-mode.md new file mode 100644 index 0000000..b1acfe9 --- /dev/null +++ b/site_extras/cpp-tools-stl-debug-mode.md @@ -0,0 +1,80 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/stl-debug-mode/) 的文章,由站点同步脚本导出。 + +# C++ STL Debug 模式 + +## 问题 + +Project阶段的schema里面的fields发生了变化,fields是一个vector节点,每个成员是`std::shared_ptr`,对比前后的vector指针发现`_M_impl`的`_M_start`位置发生了变化,因此需要gdb watch其变化,查看何处改了它。 + +## Vector结构 + +vecotr代码: + +> https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/stl_vector.h + +```cpp +typedef typename __gnu_cxx::__alloc_traits<_Tp_alloc_type>::pointer + pointer; +struct _Vector_impl_data +{ + pointer _M_start; + pointer _M_finish; + pointer _M_end_of_storage; +} +``` + +需要知道几个重要信息: + +- `_M_start`与`_M_finish`等都是pointer类型 +- pointer类型是原始指针类型 + +有了这个信息,我们watch便可以非常方便的观察这个`_M_start`发生了什么变化,注意:是这个指针`_M_start`发生了变化,而不是其值发生了变化。 + +## Watch + +首先打印其地址: + +```cpp +(gdb) p &((arrow::Schema::Impl *)this->output_schema_->impl_)->fields_._M_impl._M_start +$10 = (std::_Vector_base, std::allocator > >::pointer *) 0xb11dd0 +``` + +我们得到了`pointer* `的地址,然后watch它。 + +```cpp +(gdb) watch *((std::_Vector_base, std::allocator > >::pointer *) 0xb11dd0) +Hardware watchpoint 2: *((std::_Vector_base, std::allocator > >::pointer *) 0xb11dd0) +``` + +随后我们continue,变得了变更点: + +```cpp +(gdb) c +Continuing. + + +Thread 1 "postgres" hit Hardware watchpoint 2: *((std::_Vector_base, std::allocator > >::pointer *) 0xb11dd0) + +Old value = (std::_Vector_base, std::allocator > >::pointer) 0x979470 +New value = (std::_Vector_base, std::allocator > >::pointer) 0x9fe1e0 +VecSeqNext (node=0x9fdc40) at src/backend/vecexecutor/nodeSeqscan.c:210 +210 if (table_scan_getnextslot(scandesc, direction, slot)) +(gdb) bt +#0 VecSeqNext (node=0x9fdc40) at src/backend/vecexecutor/nodeSeqscan.c:210 +#1 0x00007f45994e5c70 in ExecVecScanFetchInternal + (node=0x9fdc40, accessMtd=0x7f45994e6335 , recheckMtd=0x7f45994e6430 ) at src/backend/vecexecutor/execScan.c:146 +#2 0x00007f45994e58da in ExecVecScanFetch (node=0x9fdc40, accessMtd=0x7f45994e6335 , recheckMtd=0x7f45994e6430 ) + at src/backend/vecexecutor/execScan.c:40 +#3 0x00007f45994e5d0b in ExecVecScan + (node=0x9fdc40, vestate=0x9fdd48, accessMtd=0x7f45994e6335 , recheckMtd=0x7f45994e6430 ) + at src/backend/vecexecutor/execScan.c:200 +#4 0x00007f45994e6487 in ExecVecSeqScan (pstate=0x9fdc40) at src/backend/vecexecutor/nodeSeqscan.c:246 +#5 0x00007f45994c40ce in VecExecProcNodeGPDB (node=0x9fdc40) at src/backend/hook/executor.c:102 +#6 0x00007f45994c41c2 in VecExecProcNodeFirst (node=0x9fdc40) at src/backend/hook/executor.c:153 +#7 0x00007f45994dc610 in ExecProcNode (node=0x9fdc40) at /home/gpadmin/install/cbdb/include/postgresql/server/executor/executor.h:281 +#8 0x00007f45994e2065 in get_current_next_batch (node=0x9fdc40) at src/backend/vecexecutor/execMain.c:1851 +#9 0x00007f4599375c76 in get_arrow_record_batch(GetNextCallback, void*) (fn=0x7f45994e204d , arg=0x9fdc40) + at c_glib/arrow-glib/reader.cpp:216 + + +``` \ No newline at end of file diff --git a/site_extras/cpp-tools-vscode-cpp-formatting.md b/site_extras/cpp-tools-vscode-cpp-formatting.md new file mode 100644 index 0000000..0080575 --- /dev/null +++ b/site_extras/cpp-tools-vscode-cpp-formatting.md @@ -0,0 +1,60 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-tools/vscode-cpp-formatting/) 的文章,由站点同步脚本导出。 + +# VS Code 格式化 C++ 代码 + +## 《C++那些事》之Step by Step格式化代码风格 + +## 0.导语 + +本节从0开始讲解如何格式化自己的代码风格,所讲内容实用vscode插件。 + +## 1.依赖 + +本次使用的是clang-format插件,具体安装比较简单: + +mac系统: + +```cpp +brew install clang-format #Mac OS +``` + +如果是ubuntu: + +``` +apt install clang-format +``` + +Windows版本的,可以在这里下载: + +> https://llvm.org/builds/ + +安装好之后,进行验证: + +```cpp +clang-format --version +``` + +**如果不想使用vscode的,可以直接clang-format裸命令使用也是可以的!** + +随后在vscode里面下载好以下两个插件: + +- c/c++ +- clang-format + +这两个在插件市场里面下载即可。 + +安装完之后,需要做一些配置,见后文。 + +## 2.配置 + +vscode设置里面可以搜索Clang_format_style,默认是file,这里可以不用改,当然你也可以设置为其他的,这里我给大家看一下这里的默认值为file,它表示读取当前文件夹或父目录里面的`.clang-format`文件进行格式化。 + +所以我们只需要生成这个文件即可,进入代码目录运行: + +```cpp +clang-format -style=google -dump-config > .clang-format +``` + +此时便会生成这个文件,这里采用google风格,你可以对这个文件自行修改。 + +对照下图设置vscode里面的clang-format插件格式,最后在文件里面alt+shift+f便可以格式化文件了,为了不必每次都按这个快捷键,当文件保存之后就格式化,大家可以勾选format on save。 \ No newline at end of file diff --git a/site_extras/how-to-build-cplusplusthings.md b/site_extras/how-to-build-cplusplusthings.md new file mode 100644 index 0000000..0d1bc7c --- /dev/null +++ b/site_extras/how-to-build-cplusplusthings.md @@ -0,0 +1,38 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/how-to-build-cplusplusthings/) 的文章,由站点同步脚本导出。 + +# 编译与运行指南 + +## 1.下载 + +安装好git,使用下面命令: + +``` +git clone git@github.com:Light-City/CPlusPlusThings.git +``` + +## 2.编译环境 + +- IDE推荐 + + [CLION](https://www.jetbrains.com/clion/) + +- 编译器 + + [GCC/G++](https://ftp.gnu.org/gnu/gcc/) + + +## 3.编译本项目 + +方式一: vscode + bazel + +[第一期:Step By Step上手编译本项目](https://www.bilibili.com/video/BV1Rv4y1H7LB/?vd_source=bb6532dcd5b1d6b26125da900adb618e) + +方式二: 免开发环境docker + +[第二期:免开发环境docker](https://www.bilibili.com/video/BV1oz4y1a7Pu/?vd_source=bb6532dcd5b1d6b26125da900adb618e) + +``` +docker pull xingfranics/cplusplusthings:latest +``` + +方式三: gcc/g++ \ No newline at end of file diff --git a/site_extras/modern-cpp-cpp11-cpp-std-move.md b/site_extras/modern-cpp-cpp11-cpp-std-move.md new file mode 100644 index 0000000..c5ac6b0 --- /dev/null +++ b/site_extras/modern-cpp-cpp11-cpp-std-move.md @@ -0,0 +1,76 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp11/cpp-std-move/) 的文章,由站点同步脚本导出。 + +# C++ std::move 原理与用法 + +`std::move` 是一个非常重要的函数,它提供了一种方式,可以将一个左值对象标记为一个特殊类型的右值对象,即 xvalue。这种转变是为了允许执行移动语义,而不是执行常规的拷贝语义。 + +- 为什么要使用 std::move +- 实际应用场景 + +## 1.为什么要使用 std::move + + + +通常情况下,只有右值对象才能进行移动操作,因为移动构造函数使用右值引用作为参数,而右值引用只能绑定到右值。因此,如果你想从一个由左值表达式表示的对象中执行移动操作,你需要使用 `std::move` 来将其转换为一个 xvalue(特殊的右值)。 + +使用 `std::move` 时需要谨慎,因为移动操作会将对象置于一个通常只允许销毁的状态。因此,最好仅对对象的最后一次使用进行移动操作。 + +`std::move` 并不实际执行移动,需要明确的是,`std::move` 并不会实际执行对象的移动操作。它只是将给定的左值对象转换为右值引用。下面是 `std::move` 的一个可能实现: + +```cpp +template +typename remove_reference::type&& move(T&& t) +{ + return static_cast::type&&>(t); +} +``` + +## 2.实际使用场景 + +### 2.1 移动构造而非拷贝构造 + +在函数 `f1` 中,参数 `a` 是一个右值引用的左值,而不是右值本身,因为它有一个名称。因此,为了强制执行 `Foo` 的移动构造,我们必须将其转换为一个右值,而 `std::move` 恰好可以完美地做到这一点。 + +```cpp +struct Foo { + ... + Foo(const Foo&) {} + Foo(Foo&&) {} +}; + +void f1(Foo&& a) { + Foo b(a); // 调用拷贝构造函数,而非移动构造函数 +} + +void f2(Foo&& a) { + Foo b(std::move(a)); // right +} + +``` + +### 2.2 所有权管理 + +假设有如下代码: + +``` +Foo* p = new Foo(); +data.push_back(p); +``` + +上述代码可以正常工作,但你无法确定指针的所有权归属。谁来删除指针?或者指针应该在何时被删除?它会在容器被销毁时被删除吗? + +在 C++11 中,`std::unique_ptr p(new Foo());` 这条语句会产生编译错误。原因在于唯一所有权规则被应用。 + +但是,你可以使用 `std::move` 来转移指针的所有权: + +``` +data.push_back(std::move(p)); +``` + +现在,指针的所有权被转移到了容器中,在容器生命周期结束时将被删除。虽然对象 `p` 仍然存在,但任何试图解引用它的尝试将生成空指针异常。 + +事实上,在移动操作之后,`p` 拥有的内部指针已经被设置为 `nullptr`。 + +综上所述,`std::move` 的作用在于提供了一种手段,能够安全地实现对象的移动语义,转变其左值属性为右值,使其可以被移动构造函数或移动赋值操作符所调用。正确地使用 `std::move` 可以提高性能并确保正确的所有权转移。 + +那么下一个问题,什么时候不需要使用std::move呢? \ No newline at end of file diff --git a/site_extras/modern-cpp-cpp11-std-bind-vs-lambda.md b/site_extras/modern-cpp-cpp11-std-bind-vs-lambda.md new file mode 100644 index 0000000..5805b5f --- /dev/null +++ b/site_extras/modern-cpp-cpp11-std-bind-vs-lambda.md @@ -0,0 +1,58 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp11/std-bind-vs-lambda/) 的文章,由站点同步脚本导出。 + +# C++ std::bind 与 lambda 对比 + +https://quick-bench.com/q/CEnR_XUuIwnZxaP4KJNSlpYG38c + +``` +#include +#include +#include + + + +struct Calculator { + int count{1}; + int total{0}; + + auto calculate() -> void { + using std::iota, std::vector; + + // Random math + vector u(8), v(Calculator::count * u.size()); + + iota(v.begin(), v.end(), *u.begin()); + + int sum{*v.begin() + *u.begin()}; + + for (size_t pos{0}; pos < v.size(); ++pos) { + sum += u[pos % u.size()] * v[pos]; + } + + Calculator::total += sum; + ++Calculator::count; + } +}; + +// auto f(std::function g) -> void { g(rand()); } + +template auto f(FN g) -> void { g(rand()); } + + +static auto BM_bind(benchmark::State& state) -> void { + Calculator calculator; + for (auto _ : state) { + f(std::bind(&Calculator::calculate, &calculator)); + } +} + +static auto BM_lambda(benchmark::State& state) -> void { + Calculator calculator; + for (auto _ : state) { + f([&calculator]([[maybe_unused]] int x) { calculator.calculate(); }); + } +} + +BENCHMARK(BM_bind); +BENCHMARK(BM_lambda); +``` \ No newline at end of file diff --git a/site_extras/modern-cpp-cpp11-variadic-templates-cpp.md b/site_extras/modern-cpp-cpp11-variadic-templates-cpp.md new file mode 100644 index 0000000..6ba49e4 --- /dev/null +++ b/site_extras/modern-cpp-cpp11-variadic-templates-cpp.md @@ -0,0 +1,146 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp11/variadic-templates-cpp/) 的文章,由站点同步脚本导出。 + +# C++ 可变参数模板 + +## 《C++那些事》之可变参数模版 + +最近在看Arrow的源代码发现了一个比较有意思的设计,在一个plan中可以添加多个节点,例如:proj、agg等等。 + +今天来讲讲这一块的设计与实现。 + +## 1.添加节点 + +对于一个plan通常需要添加一堆节点,这里便是调用EmplaceNode来实现的。 + +```cpp +return plan->EmplaceNode( + plan, std::move(inputs), schema(std::move(fields)), std::move(target_field_ids), + std::move(aggregates), std::move(kernels), std::move(states), + std::move(owned_options)); +``` + +同理,如果添加一个proj节点就是: + +```cpp +return plan->EmplaceNode(plan, std::move(inputs), + schema(std::move(fields)), std::move(exprs), + project_options.async_mode); +``` + +那么究竟是什么魅力让代码写的如此丝滑? + +这里把Plan简单写一下: + +```cpp +class ARROW_EXPORT ExecPlan +{ + public: + template + Node* EmplaceNode(Args&&... args) { + std::unique_ptr node{new Node{std::forward(args)...}}; + auto out = node.get(); + AddNode(std::move(node)); + return out; + } + } +``` + +看上这个语法非常的复杂,没错,它是11之后的可变参数模版,这里解释一下这段代码的含义,就是将参数包丢给对应的Node构造函数。这个例子可能看起来比较复杂,接下来我们进入Demo环节。 + +### 2.Demo + +接下来我们创建了一个图,图中有两个不同的节点,分别是节点 A与 节点B,为了简单器件这里使用一个函数模版EmplaceNode。 + +```cpp +NodeA* nodeA = EmplaceNode("Node A", 10, 3.14); +NodeB* nodeB = EmplaceNode(1, "Node B"); +``` + +完整代码如下: + +```cpp +#include +#include +#include + +class NodeA { +public: + NodeA(const std::string& name, int value, double weight) + : name_(name), value_(value), weight_(weight) {} + + const std::string& GetName() const { + return name_; + } + +private: + std::string name_; + int value_; + double weight_; +}; + +class NodeB { +public: + NodeB(int id, const std::string& desc) + : id_(id), desc_(desc) {} + + int GetID() const { + return id_; + } + +private: + int id_; + std::string desc_; +}; +template +Node* EmplaceNode(Args&&... args) { + std::unique_ptr node{new Node{std::forward(args)...}}; + auto out = node.get(); + // AddNode(std::move(node)); + return out; +} + +int main() { + NodeA* nodeA = EmplaceNode("Node A", 10, 3.14); + NodeB* nodeB = EmplaceNode(1, "Node B"); + std::cout << "Node A name: " << nodeA->GetName() << std::endl; + std::cout << "Node B ID: " << nodeB->GetID() << std::endl; + return 0; +} +``` + +## 3.拓展-实现一个print函数可以打印任意类型数据 + +简单实现版本: + +使用可变模版参数+递归方式实现即可。 + +每次拆分成一个+一堆,直到只剩下一个,所以有一个处理一个的函数与处理一个+一堆的函数,分别对应第一个print_impl与第二个print_impl。 + +```cpp +#include +#include + +template +void print_impl(T&& t) { + std::cout << t; +} + +template +void print_impl(T&& t, Args&&... args) { + std::cout << t; + print_impl(std::forward(args)...); +} + +template +void print(Args&&... args) { + print_impl(std::forward(args)...); +} + +int main() { + int x = 1; + const char* str = "Hello, world!"; + print("The value of x is ", x, ", and the string is ", str, ".\n"); + return 0; +} + +``` \ No newline at end of file diff --git a/site_extras/modern-cpp-cpp17-cpp-nodiscard.md b/site_extras/modern-cpp-cpp17-cpp-nodiscard.md new file mode 100644 index 0000000..4e06372 --- /dev/null +++ b/site_extras/modern-cpp-cpp17-cpp-nodiscard.md @@ -0,0 +1,78 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp17/cpp-nodiscard/) 的文章,由站点同步脚本导出。 + +# C++17 [[nodiscard]] 属性 + +自C++17引入[[nodiscard]]属性以来,我们在编写API时有了一种更强大的工具,用于标记那些在调用时不应该被忽略的函数返回值。在本文中,我们将深入探讨[[nodiscard]]的用法,并注意一些潜在的陷阱。 + +## 什么是[[nodiscard]]? + +[[nodiscard]]是一种函数属性,它告诉编译器函数的返回值不应该被忽略。这在那些依赖于返回值的函数中尤为有用,例如谓词函数或工厂函数。 + +## 如何使用[[nodiscard]]? + +在类、构造函数以及枚举上使用[[nodiscard]]都是可能的。下面是一些示例: + +```cpp +class [[nodiscard]] A {}; + +class B { +public: + [[nodiscard]] B() = default; +}; + +enum class [[nodiscard]] State { + Default, + Foo, + Bar +}; +``` + +在上述例子中,编译器将在返回值被忽略时发出警告,有助于我们及早发现潜在的bug。 + +## 警惕:[[nodiscard]]的陷阱 + +尽管[[nodiscard]]可以有效地防止返回值被无意中忽略,但我们需要注意一些潜在的陷阱。一个常见的陷阱是使用[[nodiscard]]的函数返回一个类似于作用域守卫的对象,以在作用域结束时执行清理操作(比如提交事务)。 + +```cpp +[[nodiscard]] ScopeGuard createScopeGuard() { + // Implementation of a scope guard +} + +// 在这里,用户可以选择忽略返回值,导致作用域立即销毁,从而失去清理效果 +std::ignore = createScopeGuard(); +``` + +尽管[[nodiscard]]表明函数返回值不应该被忽略,但用户仍然可以通过`std::ignore`等手段明确地忽略返回值,从而绕过清理操作。 + +## 完整示例 + +```cpp +#include // std::ignore + +class [[nodiscard]] A {}; +class B { +public: + [[nodiscard]] bool isOk() const { return true; } +}; + +// no need for nodiscard, A is nodiscard +A foo() { + return A(); +} + +// B is not nodiscard, mark it explicitly +[[nodiscard]] B bar() { + return B(); +} + +int main() +{ + foo(); // warning: ignoring temporary created by a constructor declared with 'nodiscard' attribute + bar(); // warning + + std::ignore = foo(); // ok + auto b = bar(); // ok + b.isOk(); // warning + if (b.isOk()) {} // ok +} +``` \ No newline at end of file diff --git a/site_extras/modern-cpp-cpp17-cpp-string-view.md b/site_extras/modern-cpp-cpp17-cpp-string-view.md new file mode 100644 index 0000000..7221d1d --- /dev/null +++ b/site_extras/modern-cpp-cpp17-cpp-string-view.md @@ -0,0 +1,106 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp17/cpp-string-view/) 的文章,由站点同步脚本导出。 + +# C++17 std::string_view + +## 0.问题 + +string_view为c++17之后出现,其实就是leveldb中的slice,其目的在于解决内存拷贝、高效的substr。 + +但是,也引入了指针与引用的问题,由于string_view不拥有指向内容的所有权,对比于Rust,仅仅是借用,如果拥有内容的对象提前释放,便会出现悬挂引用问题。像Rust在编译时会分析变量的生命周期,保证借用的资源在使用时不会释放。然而C++没有检查,需要人工保证。 + +### 1.内部实现 + +string_view是一个basic_string_view类型,可以看到_Traits采用默认的`char_traits`,这个很重要,后面的一些函数都采用`char_traits`的一些成员函数执行核心操作。 + +```cpp +template > + class _LIBCPP_TEMPLATE_VIS basic_string_view; + +typedef basic_string_view string_view; +``` + +basic_string_view内部成员: + +```cpp +private: + const value_type* __data; + size_type __size; +``` + +构造兼容 string与`const char*` + +```cpp +constexpr basic_string_view() noexcept; +constexpr basic_string_view(const basic_string_view&) noexcept = default; +basic_string_view& operator=(const basic_string_view&) noexcept = default; +template +constexpr basic_string_view(const charT* str); +constexpr basic_string_view(const charT* str, size_type len); +``` + +迭代器支持: + +```cpp +constexpr const_iterator begin() const noexcept; +constexpr const_iterator end() const noexcept; +constexpr const_iterator cbegin() const noexcept; +constexpr const_iterator cend() const noexcept; +const_reverse_iterator rbegin() const noexcept; +const_reverse_iterator rend() const noexcept; +const_reverse_iterator crbegin() const noexcept; +const_reverse_iterator crend() const noexcept; +``` + +其他核心函数: + +- substr +- remove_prefix +- compare +- find +- ... + +从c++20开始支持starts_with、ends_with、contains,这三个在leveldb的slice中一开始就支持了。 + +`char*`的string_view字面量: + +```cpp +constexpr basic_string_view operator "" sv( const char *str, size_t len ) noexcept; +constexpr basic_string_view operator "" sv( const wchar_t *str, size_t len ) noexcept; +constexpr basic_string_view operator "" sv( const char16_t *str, size_t len ) noexcept; +constexpr basic_string_view operator "" sv( const char32_t *str, size_t len ) noexcept; +``` + +## 2.使用陷阱 + +### 2.1 `\0` + +string_view不关心`\0`,会关心实际的字符个数。 + +```cpp +std::string_view s = "abc\0\0def"sv; +std::cout << "s: " << s.size() << " \"" << s << "\"\n"; +``` + +输出:s: 8 "abcdef" + +```cpp +std::string_view str{"hello", 1}; +std::cout << str.data() << std::endl; // 输出 hello,而不是h +``` + +这里预期输出hello,但是却输出了h。 + +### 2.2 生命周期 + +```cpp +string_view foo() { + std::string s{"hello world"}; + return string_view{s}; +} +``` + +在该示例中,s出了作用域,string_view变成了悬空引用。 + + +!!! warning "生命周期" + `std::string_view` 不拥有底层字符存储,务必保证被引用对象的生命周期覆盖全部使用期。 \ No newline at end of file diff --git a/site_extras/modern-cpp-cpp17-cpp17-pmr.md b/site_extras/modern-cpp-cpp17-cpp17-pmr.md new file mode 100644 index 0000000..c0ec432 --- /dev/null +++ b/site_extras/modern-cpp-cpp17-cpp17-pmr.md @@ -0,0 +1,309 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp17/cpp17-pmr/) 的文章,由站点同步脚本导出。 + +# C++17 PMR 内存资源 + +[TOC] + +## 1.引入 + +在C++17之前,标准库提供了`std::allocator`,而在C++17中,这一功能得到了加强,引入了`polymorphic_allocator`。 + +在C++17之前,如果我们想要使用`std::allocator`来自定义内存池,我们不能使用传统的虚拟多态方式,因为`std::allocator`并没有提供虚拟函数。因此,通过继承显然不是一个好的选择,但是我们可以将其作为类成员使用,如下所示: + +```cpp +template > +class MemoryPool { + public: + explicit STLMemoryPool(const Allocator& alloc) : alloc_(alloc) {} + + Status Allocate(int64_t size, uint8_t** out) { + try { + *out = alloc_.allocate(size); + } catch (std::bad_alloc& e) { + return Status::OutOfMemory(e.what()); + } + return Status::OK(); + } + + void Free(uint8_t* buffer, int64_t size) { + alloc_.deallocate(buffer, size); + } + + private: + Allocator alloc_; +}; +``` + +在C++17之前如果我们想在内存分配/释放时做一些print操作,或者一些自定义操作,可以使用两种办法: + +- 自定义全局的new/delete + +```cpp +void* operator new(std::size_t size) { + void* ptr = malloc(size); + std::cout << "Allocated: " << size << " bytes at address " << ptr << std::endl; + return ptr; +} + +void operator delete(void* ptr, std::size_t n) noexcept { + std::cout << "Deallocated: " << n << " bytes at address " << ptr << std::endl; + free(ptr); +} +``` + +- 自定义allocator + +```cpp +class allocator { + public: + using value_type = T; + value_type* allocate(std::size_t n) { + value_type* p = static_cast(::operator new(n * sizeof(value_type))); + std::cout << "Allocated: " << n * sizeof(value_type) << " bytes at address " + << static_cast(p) << std::endl; + return p; + } + + void deallocate(value_type* p, std::size_t n) noexcept { + std::cout << "Deallocated: " << n * sizeof(value_type) << " bytes at address " + << static_cast(p) << std::endl; + ::operator delete(p); + } +}; + +std::vector> vec; +``` + +那么,除此之外,还有哪些办法呢? + +C++17之后,我们可以通过使用多态内存管理工具,polymorphic memory resources(pmr),使用方式: + +```cpp +#include + +pmr::monotonic_buffer_resource res(10); +pmr::vector> vec1(&res); +``` + +对于上面这个问题,我们可以通过继承memory_resource,有关下面代码的解释见下方memory_resource。 + +```cpp +class TrackAllocator : public std::pmr::memory_resource { + void* do_allocate(std::size_t bytes, std::size_t alignment) override { + void* p = std::pmr::new_delete_resource()->allocate(bytes, alignment); + std::cout << "do_allocate: " << bytes << " bytes at " << p + << " alignment: " << alignment << std::endl; + return p; + } + + void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override { + std::cout << "do_deallocate: " << bytes << " bytes at " + << " alignment: " << alignment << std::endl; + return std::pmr::new_delete_resource()->deallocate(p, bytes, alignment); + } + + bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override { + return std::pmr::new_delete_resource()->is_equal(other); + } +}; +``` + +至此,我们便做到了上面的效果。 + +## 2.memory_resource + +STL中std::par::memory_resource类的实现比较简单,以下为一个伪代码实现: + +```cpp +class memory_resource { + static const size_t __max_align = alignof(max_align_t); + +public: + virtual ~memory_resource(); + + void* allocate(size_t __bytes, size_t __align = __max_align) { + return do_allocate(__bytes, __align); + } + + void deallocate(void* __p, size_t __bytes, size_t __align = __max_align) { + do_deallocate(__p, __bytes, __align); + } + + bool is_equal(const memory_resource& __other) const noexcept { + return do_is_equal(__other); + } + +private: + virtual void* do_allocate(size_t, size_t) = 0; + virtual void do_deallocate(void*, size_t, size_t) = 0; + virtual bool do_is_equal(memory_resource const&) const noexcept = 0; +}; +``` + +与默认的std::allocator不同,memory_resource提供了多态的能力,允许用户通过继承的方式,重写以下三个接口: + +- do_allocate + - 用于分配指定大小和对齐要求的内存。 +- do_deallocate + - 释放之前通过 `do_allocate` 分配的内存。 +- do_is_equal + - 检查当前内存资源对象是否与另一个内存资源对象相等。 + +于是,我们上方的TrackAllocator通过重写上面三个函数接口即可。 + +细心的朋友发现,除了这几个接口,memory_resource也提供了对外的三个接口。除此之外,提供了全局的接口: + +```cpp +memory_resource* new_delete_resource() noexcept; +memory_resource* null_memory_resource() noexcept; +memory_resource* get_default_resource() noexcept; +memory_resource* set_default_resource(memory_resource* __r) noexcept; +``` + +- new_delete_resource + +返回一个`memory_resource`指针,该指针使用`new`和`delete`运算符来分配和释放内存。这个资源是使用全局的`new`和`delete`运算符实现的,因此它是默认的内存资源管理器。这个memory_resource子类是`__resource_adaptor_imp`,它会负责重写上面三个接口。 + +- null_memory_resource + +返回一个`memory_resource`指针,该指针表示一个不执行任何操作的空内存资源。当你想要在不进行实际内存分配的情况下测试或占位时,可以使用这个资源。STL源码当中的实现中定义了一个 + +```cpp +inline memory_resource* +null_memory_resource() noexcept { + class type final : public memory_resource {}; +} +``` + +type作为子类重写了上面三个接口,do_allocate接口会跑出分配错误的异常,因为语义就是不允许分配了。 + +``` +do_allocate(size_t, size_t) override + { std::__throw_bad_alloc(); } +``` + +所以当我们使用null_memory_resource,需要try catch,例如:string字符小于16是在stack上分配,此时不会用到我们这里的memory_resource,所以正常运行,当长度大于等于16,那么就会跑出std::bad_alloc异常。 + +```cpp +std::pmr::memory_resource* default_resource = std::pmr::null_memory_resource(); + +std::pmr::set_default_resource(default_resource); + +try { + std::pmr::vector data{1}; +} catch (std::bad_alloc e) { + std::cerr << "bad_alloc is thrown vector" << std::endl; +} + +std::pmr::string str1{"hello world hel"}; +std::cerr << str1 << " length:" << str1.size() << std::endl; + +try { + std::pmr::string str1{"hello world hel0"}; +} catch (std::bad_alloc e) { + std::cerr << "bad_alloc is thrown string" << std::endl; +} +``` + +- get_default_resource + +返回当前默认的全局内存资源管理器。默认情况下,它是一个与`new_delete_resource()`返回的相同资源,但通过`set_default_resource`函数可以更改默认资源。 + +- set_default_resource + +设置内存资源管理器,参数可以为空(返回默认资源),否则设置为传递的资源,并返回之前的默认资源。 + +## 3.内存复用 + +std::pmr::monotonic_buffer_resource是一个缓存区,在内存复用方面具有重要的优势。monotonic_buffer_resource继承memory_resource,重写上面三个接口,内部会持有上游的memory_resource,如果当前内存不足,便会从上游去取。这有助于降低内存碎片,提高内存利用率。 + +下面列出一些有关std::pmr::monotonic的使用方法。 + +**用法1 - 使用固定缓冲区创建 `monotonic_buffer_resource`:** + +```cpp +char buf1[30] = {}; +std::fill_n(std::begin(buf1), std::size(buf1) - 1, '_'); +std::pmr::monotonic_buffer_resource pool1{std::data(buf1), std::size(buf1)}; +std::pmr::vector myVec1{&pool1}; +``` + +**用法2 - 利用上游资源创建 `monotonic_buffer_resource`:** + +```cpp +std::pmr::memory_resource* upstreamResource2 = std::pmr::get_default_resource(); +std::pmr::monotonic_buffer_resource pool2(upstreamResource2); +{ + std::pmr::vector numbers(&pool2); + for (double d = 0.1; d < 1.0; d += 0.1) { + numbers.push_back(d); + } +} +``` + +**用法3 - 继承std::pmr::monotonic_buffer_resource:** + +```cpp +class MyMonotonicBufferResource : public std::pmr::monotonic_buffer_resource { +public: + using std::pmr::monotonic_buffer_resource::monotonic_buffer_resource; // Inherit constructors + + // Public function to access do_allocate + void* allocate(std::size_t bytes, std::size_t alignment) { + return do_allocate(bytes, alignment); + } + + // Public function to access do_deallocate + void deallocate(void* p, std::size_t bytes, std::size_t alignment) { + do_deallocate(p, bytes, alignment); + } +}; + +MyMonotonicBufferResource pool3; +// Allocate memory +void* mem = pool3.allocate(100, alignof(int)); + +// Use the allocated memory (this is just an example) +std::pmr::vector numbers({1, 2, 3}, &pool3); + +// Deallocate memory +pool3.deallocate(mem, 100, alignof(int)); + +std::cout << "Usage 3 - Inherit: \n\n"; +``` + +## 4.pool resource + +synchronized_pool_resource这是一个线程安全的内存池资源类。它的设计目的是在多线程环境中安全地进行内存分配和释放。当多个线程并发地尝试进行内存分配或释放时,`synchronized_pool_resource` 使用同步机制确保线程安全性。这通常会带来一些性能开销,因为需要加锁来保护共享的内存池。 + +与之对应的是unsynchronized_pool_resource,需要用户自己保证线程安全。 + +synchronized_pool_resource源码实现其实就是mutex + unsynchronized_pool_resource。 + +```cpp +class synchronized_pool_resource : public memory_resource { + +public: + // some interface +private: + mutex __mut_; + unsynchronized_pool_resource __unsync_; +}; +``` + +pool_options可以用来配置这些内存池。 + +```cpp +struct pool_options { + size_t max_blocks_per_chunk = 0; + size_t largest_required_pool_block = 0; +}; +``` + +- max_blocks_per_chunk + +表示每个块中最多可以包含的块数量。用于限制内存池从上游内存资源一次性获取的内存量。如果设置为 0,内核会通过munge_options函数去修改pool_options。 + +- largest_required_pool_block + +表示内存池能够处理的最大块大小。 \ No newline at end of file diff --git a/site_extras/modern-cpp-cpp17-cpp17-top-17-features.md b/site_extras/modern-cpp-cpp17-cpp17-top-17-features.md new file mode 100644 index 0000000..7df90b9 --- /dev/null +++ b/site_extras/modern-cpp-cpp17-cpp17-top-17-features.md @@ -0,0 +1,330 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp17/cpp17-top-17-features/) 的文章,由站点同步脚本导出。 + +# C++17 十七项高级特性 + +### 全面盘点17个C++17的高级特性 + + + +C++17是目前比较常用的版本之一,今天花时间来梳理一下17个重要特性,所有的特性也不止这么点。 + +[TOC] + +### 1. 并行算法 + +C++17引入了许多并行版本的标准库中的算法。这些算法可以并行执行,因此在多核系统上可能会带来显著的性能提升。 + +之前写过一篇全面介绍这个特性的文章,可以看这篇: + +例子: + +```cpp +#include +#include +#include +int main() { + std::vector v = {1, 2, 3, 4, 5}; + std::sort(std::execution::par, v.begin(), v.end()); +} +``` + +在此例子中,`std::sort`是并行执行的,以并行方式对向量`v`的元素进行排序。 + +### 2. If Initializers + +C++17中的If初始化器是一项特性,它允许在if语句中直接初始化变量。这种初始化方式在一定程度上可以提高代码的可读性和简洁性。 + +在传统的C++中,我们通常会这样初始化变量: + +```cpp +int x; +if (condition) { + x = 42; +} else { + x = 24; +} +``` + +而在C++17中,可以使用if初始化器来简化这个过程,使代码更加紧凑: + +```cpp +if (bool condition = /* some condition */) { + int x = 42; +} else { + int x = 24; +} +``` + +在这个例子中,我们将变量`x`的初始化直接放在if语句中。变量`condition`在if语句中被定义和初始化,然后在if语句块中可用。这种方式更加直观和简洁,尤其是在简单的条件初始化时。 + +### 3. 类模板参数推导(CTAD) + +CTAD 让编译器从函数参数中自动推导出模板参数。这使得在不必显式指定模板参数的情况下更容易地使用模板。 + +往期对这个特性的全面阐述文章: + +例子: + +``` +template +void foo(T t) { + // ... +} + +int main() { + foo(42); // 编译器推导出T的类型为int +} +``` + +在此例子中,当调用`foo(42)`时,编译器推导出`T`的类型是`int`. + +### 4. template `` + +模板关键词被引入为非类型模板参数的占位符。它允许在模板中表示任何类型的值。 + +例子: + +``` +template +struct constant { + static constexpr auto get_value() { return value; } +}; + +// 用法 +static_assert(constant<42>::get_value() == 42); +``` + +### 5. std::optional 和 std::variant + +`std::optional` 和 `std::variant` 是C++17中引入的两个新类型。`std::optional` 表示一个可能存在也可能不存在的值,`std::variant` 代表一个类型安全的联合,可以保存不同类型的值。 + +例子: + +``` +#include +#include + +int main() { + std::optional opt = 42; + std::variant var = 3.14; +} +``` + +在这个例子中,`opt`是包含值42的可选整数,`var`是包含值3.14的变体。 + +### 6. 折叠表达式 + +在C++17中,折叠表达式提供了一种简洁的方式,用于对参数包执行二元操作。它们允许在不需要显式递归或迭代的情况下执行诸如求和、乘法或连接参数包中元素的操作。 + +例如: + +```cpp +#include + +template +T sum(T t) { + return t; +} + +// 使用折叠表达式的递归情况 +template +T sum(T first, Args... args) { + return first + sum(args...); +} + +int main() { + int total = sum(1, 2, 3, 4, 5); + std::cout << "总和: " << total << std::endl; + + return 0; +} + +``` + +递归`sum`函数中的折叠表达式`(first + ... + args)`对参数包中的每个元素应用了加法操作。 + +### 7. 结构化绑定 + +结构化绑定允许你将对象分解成其构成元素,类似于你可能会用到的元组拆包。 + +往期文章: + +例子: + +``` +#include +#include +int main() { + std::tuple t(42, "hello", 3.14); + auto [i, s, d] = t; // i = 42, s = "hello", d = 3.14 +} +``` + +在此例子中,结构化绑定`[i, s, d]`将元组`t`分解成其构成元素。 + +### 8.模板模板参数 + +例如:在C++17中,语法 `templatetypename bob> struct foo {}` 声明了一个名为 `foo` 的模板,它接受一个名为 `bob` 的模板模板参数。模板模板参数 `bob` 本身接受任意数量的模板类型参数。 + +```cpp +template typename bob> +struct foo { + template + void bar(const bob& arg) { + std::cout << "size: " << arg.size() << std::endl; + } +}; + +int main() { + foo f; + std::vector vec = {1, 2, 3, 4, 5}; + f.bar(vec); + + return 0; +} +``` + +在 `main` 函数中,我们使用 `std::vector` 实例化了 `foo`,将其作为 `bob` 的模板参数。这使我们能够创建一个通用的结构 `foo`,可以与任何接受任意数量类型参数的模板一起工作,例如 `std::vector`、`std::list` 或用户定义的模板。 + +### 9. 内联变量 + +C++17允许在类的定义内部定义变量为内联的,这可以帮助减小二进制大小,可能通过防止变量在多个转换单元中的重复副本来提高性能。 + +例子: + +```cpp +class MyClass { +public: + inline static int inlineVar = 42; +}; + +int main() { + int localVar = MyClass::inlineVar; +} +``` + +在这里,`inlineVar`是`MyClass`的内联静态成员变量。 + +### 10. 属性改进 + +C++17提供新的属性,并改进了已有的属性,允许开发人员为编译器提供更多的代码行为信息。 + +例子: + +```cpp +[[nodiscard]] int get_sum(int a, int b) { + return a + b; +} + +int main() { + auto result = get_sum(1, 2); + // 编译器可能会警告‘result’未使用 +} +``` + +在此例子中,`[[nodiscard]]`是可以应用于函数的属性,表示其返回值不应该被调用者丢弃。 + +### 11. 嵌套命名空间 + +C++17通过折叠表达式增强了变参模板,使得在处理参数包时的代码更为简洁和表达明了。 + +例子: + +```cpp +// 外部命名空间 +namespace outer { + // 内部命名空间 + namespace inner { + void foo() { + std::cout << "在内部命名空间中" << std::endl; + } + } +} + +outer::inner::foo(); + +``` + +嵌套命名空间定义提供了一种将代码层次化组织的方式,提高了可读性和可维护性,尤其是在大型项目中。它们还通过提供更加结构化的命名空间层次结构来帮助避免命名冲突。 + +### 12. 字面量改进 + +C++17增强了字面量,包括对整数和浮点字面量的改进,以及对真和假字面量的支持。 + +例子: + +``` +auto num = 123_456; // Underscore in integer literals +auto pi = 3.1415_f; // Suffix for floating-point literals +``` + +### 13. constexpr Lambda + +C++17允许lambda函数成为constexpr,如果它们满足条件,就可以在需要编译时评估的上下文中使用,例如: + +```cpp +constexpr auto lambda = [](int x) { return x * 2; }; +static_assert(lambda(5) == 10); +``` + +在这个例子中,`lambda`是一个constexpr lambda,它接受一个整数x作为参数,然后返回x的两倍。`static_assert`检查在编译时,`lambda(5)`的值是否等于10。 + +### 14. 捕获*this + +在lambda中捕获*this变得更加简单,允许直接访问包含对象的成员。 + +```cpp +class Foo { + int data = 42; +public: + auto member_lambda() { + return [*this] { std::cout << data << std::endl; }; + } +}; + +// 使用 +Foo f; +f.member_lambda()(); // 输出 "42" +``` + +### 15. 扩展的if和switch语句 + +`if`或`switch`语句中的条件现在可以是任何表达式,不仅限于布尔条件。这使得控制流更加灵活,例如使用结构化绑定时: + +```cpp +if (const auto [it, inserted] = map.insert({"foo", bar}); inserted) { + // ... +} +``` + +在此例子中,`if`语句检查`inserted`变量是否为真,但条件还包括结构化绑定的赋值。 + +### 16. 泛化的基于范围的for循环 + +此改进支持不同于起始迭代器类型的标志或结束迭代器,这有助于处理以空终止的循环和其他类似情况。例如: + +```cpp +for (auto it = my_container.begin(); it != my_container.end(); ++it) { + // ... +} +``` + +在此例子中,`my_container`可能是使用不同类型的结束迭代器的容器,但循环仍然可以正确工作。 + +### 17. if constexpr + +此特性通过允许编译器在编译时评估条件,从而实现更通用的代码。如果条件为真,则编译的代码中包含`if`块内的代码;否则,它将被丢弃。这可以通过在运行时删除不必要的分支来简化代码。例如: + +```cpp +if constexpr (std::is_same_v) { + // 用于int的特定代码 +} else { + // 用于其他类型的代码 +} +``` + +在这个例子中,`if constexpr`语句检查类型`T`是否为`int`,并相应地包含适当的代码。 + +### 总结 + +本节就先写这么多,其他内容下一节进行阐述。 \ No newline at end of file diff --git a/site_extras/modern-cpp-cpp17-ctad-class-template-argument-deduction.md b/site_extras/modern-cpp-cpp17-ctad-class-template-argument-deduction.md new file mode 100644 index 0000000..6984b0b --- /dev/null +++ b/site_extras/modern-cpp-cpp17-ctad-class-template-argument-deduction.md @@ -0,0 +1,118 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp17/ctad-class-template-argument-deduction/) 的文章,由站点同步脚本导出。 + +# C++17 类模板参数推导 CTAD + +## **引入** + +C++模板元编程一直是C++开发者们熟知的一项功能,无论是初学者还是高级开发者都能够通过它编写具有类型参数的通用代码。随着C++17引入的类模板参数推导(CTAD),处理类模板的过程变得更加直观和灵活。 + +本节所有代码也会更新于社群中。 + +C++17引入CTAD会带来如下好处: + +例如:这么复杂的代码 + +``` +std::vector> obj{a, b, c}; +``` + +我们只需要: + +``` + std::vector obj{a, b, c}; +``` + +再比如: + +``` +std::tuple +``` + +可以简化为: + +``` +std::tuple t1{1}; +``` + +所以,咱们今天来看看CTAD。 + +CTAD全称是Class template argument deduction (CTAD),类模版参数推导,你给定编译器一个推导指南(deduction guide),我们便可以使用这个特性了。 + +如果不给定,如下面例子: + +``` +#include +template +struct MyPair { + T first{}; + U second{}; +}; + +int main() { + MyPair p1{1, 2}; + MyPair p2{1, 2}; + std::cout << p1.first << ", " << p1.second << std::endl; + std::cout << p2.first << ", " << p2.second << std::endl; + return 0; +} +``` + +对于p2,我们便会报错: + +no viable constructor or deduction guide for deduction of template arguments of 'MyPair' + +那么对于怎么修改呢? + +只需要添加deduction guid即可,如下写法即可。 + +``` +template +MyPair(T, U) -> MyPair; +``` + +类模板参数推导(CTAD)通过允许编译器从构造函数参数中推导出模板参数,简化了类模板的实例化过程。在引入CTAD之前,开发者在实例化时必须明确指定模板参数。然而,通过CTAD,这种明确的指定变得不再必要,从而使代码更易读、易维护。 + +``` +template +class Add{ + private: + T first; + T second; + public: + Add() = default; + Add(T first, T second): first_{first}, second_{second} {} + T result() const { return first + second; } +}; + +int main(){ + Add one(1,2); // T被推导为int + Add two{1.245, 3.1415}; // T被推导为double + Add three = {0.24f, 0.34f}; // T被推导为float +} +``` + +在上面的示例中,CTAD允许类型`T`基于构造函数参数被推导,消除了显式类型指定的需求。 + +## **非静态成员初始化** + +但是有个场景用不了CTAD,那就是非静态成员初始化。例如:Test类中使用外面的MyPair就必须写全。 + +``` +template +struct MyPair { + T first{}; + U second{}; +}; + +template +MyPair(T, U) -> MyPair; + +class Test { + MyPair p4; // use of class template 'MyPair' requires template arguments; argument + // deduction not allowed in non-static class member +}; +``` + +在上面的例子中,我们定义了一个模板类`MyPair`,并在其中使用了CTAD。然而,当我们尝试在类`Test`中使用`MyPair`作为非静态成员时,由于非静态成员初始化的上下文,CTAD将无法正常工作,导致编译错误。在这种情况下,我们必须显式指定`MyPair`的模板参数。 + +我们可以看到在非静态成员初始化的上下文中,CTAD无法推导模板参数。因此,在类模板作为非静态成员时,开发者需要显式指定所有的模板参数。这一点需要在实际开发中特别注意,以确保代码的正确性和可维护性。 \ No newline at end of file diff --git a/site_extras/modern-cpp-cpp17-optional-ref.md b/site_extras/modern-cpp-cpp17-optional-ref.md new file mode 100644 index 0000000..8b3aaa8 --- /dev/null +++ b/site_extras/modern-cpp-cpp17-optional-ref.md @@ -0,0 +1,170 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp17/optional-ref/) 的文章,由站点同步脚本导出。 + +# C++17 optional_ref + +This should have been part 2 of my [comparison series](https://www.foonathan.net/blog/2018/06/20/equivalence-relations.html), and I have almost finished it, but due to university stuff I just haven’t found the time to polish it. + +But the optional discussion started again, so I just wanted to *really* quickly share my raw thoughts on the topic. In case you are lucky and don’t know what I mean: `std::optional` doesn’t compile right now, because the behavior of assignment wasn’t clear (even though it actually is). There are basically four questions in the discussion I want to answer: + +1. Is `std::optional` the same as a pointer? +2. Do we need `std::optional`? +3. Should the assignment operator rebind or assign through? +4. Should it even have an assignment operator? + +tl;dr: no, I don’t, rebind, no. + + + +## 1. Is `std::optional` the same as a pointer? + +What does it even mean to have an “optional `T&`”? Well, it is a `T&` that can also be `nullptr`. + +So a pointer, a `T*`? + +No, not really. + +There is a more important difference between `T&` and `T*` besides the nullability: A `T&` has implicit creation and access, a `T*` explicit creation and access. + +If you have an object, you can just silently bind a reference to it. And if you have a reference, you can just treat it *as if* it was the object. Whereas for pointers, you need to explicitly use `&obj` and `*ptr`. + +And this difference is huge: It means `const T&` can be used for function parameters without any additional syntax issues: + +```cpp +void print(const T& obj); +… +T obj = …; +print(obj); +``` + +You wouldn’t want to use a `const T*` as now the call side has to do extra work, it has to use the unnecessary `&obj`. This is just awkward. + +> It is a different situation if the function might modify the object through the pointer/reference, or store a pointer/reference in a persistent location. + +So naturally, if you want to have an optional argument, you wouldn’t want to use a pointer for the same reason: Why now introduce unnecessary syntactic overhead? It shouldn’t matter to the caller. + +So `std::optional` is not the same as `T*`: It would have implicit creation syntax, not explicit. + +What `std::optional` cannot have, however, is implicit access. Not only is it not implementable currently, it is also fundamentally impossible: For `std::optional` to have implicit access syntax, every operation on it would delegate to the referring object. This includes checking whether it refers to an object! Any `.has_value()` or `!opt` would forward to the referring object. This means that an explicit syntax is required, otherwise you’re just checking whether the optional has an object that is null. + +A more thorough analysis can be found in the first twenty minutes of my [Rethinking Pointers](https://jonathanmueller.dev/talk/cppnow2018) talk at C++Now earlier this year. + +## 2. Do we need `std::optional`? + +As `std::optional` isn’t the same as `T*`, we need to look at the situations where we use `T&` and think about whether we need an optional version there. + +Luckily, I did exactly that in my [Rethinking Pointers](https://jonathanmueller.dev/talk/cppnow2018) talk. + +### Function Parameters + +```cpp +void print(const T& obj); +void sort(Container& cont); +``` + +Here we want to either avoid a copy, or modify an argument in-place. If we want to have optional arguments, a `std::optional` is a solution. However, simply overloading the function works as well. + +### Getter Functions + +```cpp +const std::string& person::name() const; +``` + +Again, we want to avoid a copy. + +If the returned value might not be available, we could just use non-reference `std::optional`, but have to pay for an additional copy. Or we could narrow the contact and add a precondition requiring the object to be there, but this is less type safe. + +### LValue Functions + +```cpp +T& std::vector::operator[](std::size_t index); +T& std::optional::value(); +``` + +Here we *absolutely* need an lvalue as a return type. This is the motivation behind references, so we use them. However, optional references wouldn’t work – we’d loose implicit access, which is incompatible with the conventional usage of operators. + +### Range-based `for` loops + +```cpp +for (auto& cur : container) + … +``` + +Here optional references are not required. + +### Lifetime extension when calling a function (experts only): + +```cpp +const std::string& name = p.name(); +// use `name` multiple times +``` + +Lifetime extension only works with normal references. + +------ + +That’s it, that are all the situations where you should use a `T&`. The only situations where it might be feasible to have a `std::optional` are function parameters and getters where we want to avoid a copy. This is not such a compelling use-case. + +## 3. Should the assignment operator rebind or assign through? + +Assignment *fundamentally* is an optimization of copy. It should just do the same thing as “destroy the current object” and “copy a new one over”. + +So when we write `opt_a = opt_b`, it will modify `opt_a` so it is a copy of `opt_b`. This is true for all `T`, including `T&`: If `opt_b` is a reference to `my_obj`, then `opt_a` will also be a reference to `my_obj`, even it was a reference to `other_obj` before. So the copy assignment operator does a rebind operation. + +Now `std::optional` also has an assignment operator taking a `T`: This assignment operator is an optimization of the constructor taking a `T`. + +> Read more about assignment as an optimization in [this blog post](https://www.foonathan.net/blog/2017/10/11/explicit-assignment.html) I’ve written some time ago. + +As such, it will destroy the current object, if there is any, and then create the new object inside it. However, as it is an optimization, it will use `T::operator=` if the optional has a value already. The assignment operator of `T` might be more efficient than “destroy” followed by “construct”. + +But note that it only does that, because it assumes that the assignment operator of `T` is an optimization of copy! If you provide a `T` where `rocket = launch` means “launch the rocket” this will fail. But this isn’t optional’s fault, your type is just stupid! + +And one such stupid type is `T&`: The assignment operator of `T&` is not an optimization of “destroy” followed by “copy”. This is because references *have no assignment operator*: Every operation you do on a reference is actually done on the object it refers to. This includes assignment, so the assignment operator will assign the value, it assigns through. + +Now some people think that having that behavior in the `operator=` of `optional` itself is even a possibility they need to consider. + +It isn’t. + +It absolutely isn’t. + +Ignoring any other counter argument, those semantics would lead to confusion as `operator=` would do completely different things depending on the state of the `optional`! + +```cpp +std::optional opt = …; + +T obj; +opt = obj; +// if opt was empty before, it will now refer to obj +// if opt wasn't empty before, it will now refer to an object with the same value as obj + +return opt; // so this is legal only if the optional wasn't empty before +``` + +There is no precedent for an assignment operator that behaves like this, because an assignment operator shouldn’t behave like this. + +## 4. Should it even have an assignment operator? + +Whenever we use a `T&` we don’t need to modify the reference itself – after all, we can’t. So when we replace the `T&` with a `std::optional` there is no need to mutate the `std::optional` either. + +Now the “assign through” people of `std::optional` argue that this behavior is consistent with `T&`. + +It isn’t, as references aren’t assignable. + +Sure, writing `ref = obj` *compiles*, but it is not an assignment. It only works because *every* operation done on a reference is done on the object it refers to. + +Now as I said before, when we have a nullable reference we can’t do that, because then we would have no syntax to check for nullability. So the only way to be truly consistent with `T&` would be if `std::optional` would have no modifying operators. It shouldn’t have an `operator=`, an `emplace()` function, etc. After all, `T&` is immutable, so `std::optional` should be as well. + +If you are in a situation where you need to mutate a `std::optional`, you didn’t *want* an `std::optional`, you wanted a pointer. Because then you store the optional in a persistent location and should have used an explicit creation syntax to make it obvious. More on that in [my talk](https://jonathanmueller.dev/talk/cppnow2018). + +Note that, if you have a `std::optional` without modifiers, it behaves nothing like an `std::optional` – because a `T&` behaves nothing like a `T`. Just like generic code can’t handle `T&`, it also wouldn’t handle `std::optional`. + +So we shouldn’t spell “optional `T&`” as `std::optional`, it should be spelt differently. I’d argue it should be called `std::optional_arg`, because that reflects the actual use case it’s going to get. + +## Conclusion + +In my opinion we don’t need `std::optional`. It is a weird type with only very few use cases. + +If the committee decides that adding `std::optional` is worth the effort, it should be an immutable `std::optional`, just like references are. For the actual uses cases of `std::optional`, just like the use cases of `T&`, it doesn’t actually matter. + +Note that a type that behaves like a `T*`, but isn’t, is useful: A `T*` can do a lot of different things, so it might be a good idea to add a distinct type that explicitly models just one of the things it does. In my [type_safe](https://github.com/foonathan/type_safe) library, for example, I have an `ts::optional_ref`, which is like a `T*` and not like a nullable `T&`. However, it definitely shouldn’t be spelled `std::optional`, because it is not a `T&`. + +More details, again, in my [Rethinking Pointers](https://jonathanmueller.dev/talk/cppnow2018) talk. \ No newline at end of file diff --git a/site_extras/modern-cpp-cpp17-std-variant.md b/site_extras/modern-cpp-cpp17-std-variant.md new file mode 100644 index 0000000..8fdc228 --- /dev/null +++ b/site_extras/modern-cpp-cpp17-std-variant.md @@ -0,0 +1,58 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp17/std-variant/) 的文章,由站点同步脚本导出。 + +# C++17 std::variant + +最近想出一些有趣的问题,做一个小总结,你能回答上来几个? + +注:完整答案会在星球揭秘。 + +**问题1:std::variant构造函数会调用几个类型构造,1个?2个?n个?** + +```cpp +std::variant a1; +std::variant a2; +``` + +**相关问题:** + +1. `std::variant` 的默认构造行为如何? +2. 当 `std::variant` 包含类型不具有默认构造函数时会发生什么? +3. 如何指定 `std::variant` 的初始值类型? + +**问题2:下面代码可以正常运行吗?如果可以运行说明理由,否则给出几个解决方案。** + +```cpp +struct X { + X(int v) : v_(v) {} + int v_; +}; + +struct Y { + Y(double v) : v_(v) {} + double v_; +}; + +std::variant a1; +``` + +**相关问题:** + +1. 如何为 `std::variant` 添加具有默认构造函数的类型? +2. 当 `std::variant` 的类型列表中没有默认构造函数时,有哪些解决方案? +3. 如何显式构造 `std::variant` 以避免默认构造问题? + +**问题3:什么是monostate,解决了什么问题?** + +**相关问题:** + +1. 如何在 `std::variant` 中使用 `std::monostate`? +2. `std::monostate` 的引入对 `std::variant` 的设计有哪些影响? +3. 在什么情况下使用 `std::monostate` 是合适的? + +**问题4:defer_lock_t、try_to_lock_t、adopt_lock_t这三者的区别是什么?什么场景下使用?** + +**相关问题:** + +1. `defer_lock_t` 的使用场景和实现机制是什么? +2. `try_to_lock_t` 如何处理互斥锁的尝试锁定? +3. `adopt_lock_t` 在接管已有锁的所有权时有什么注意事项? \ No newline at end of file diff --git a/site_extras/modern-cpp-cpp17-structured-bindings.md b/site_extras/modern-cpp-cpp17-structured-bindings.md new file mode 100644 index 0000000..fa46987 --- /dev/null +++ b/site_extras/modern-cpp-cpp17-structured-bindings.md @@ -0,0 +1,117 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp17/structured-bindings/) 的文章,由站点同步脚本导出。 + +# C++17 结构化绑定 + +在聊结构化绑定之前,有几个面试问题,看看你会不会? + +- 如何使用结构化绑定访问自定义类的私有成员? + +- 如何使用结构化绑定修改自定义类的成员呢? + +这几个题目估计没几个人能答上来,题目与答案网上也都没有,基于这些问题,我们引出本文的内容:如何为自定义类型支持结构化绑定,里面有哪些坑? + +**注:懒人版,答案与代码已放星球。** + +为了让本文的内容更加丝滑,从示例出发。 + +## 1.自定义类型 + +如果给定一个类: + +```cpp +struct Foo { + Foo(int64_t id, std::string data) : id_(id), data_(std::move(data)) {} + void Print() { std::cout << "id = " << id_ << ", data = " << data_ << std::endl; } + + private: + int64_t id_; + std::string data_; +}; +``` + +要让下面这个代码运行起来: + +```cpp +int main() { + Foo f{12, "hello world"}; + const auto& [id, data] = f; + std::cout << "id = " << id << ", data = " << data << std::endl; + f.Print(); +} +``` + +如果直接运行会编译: + +```cpp + cannot decompose inaccessible member 'Foo::id_' of 'const Foo' + 42 | const auto& [id, data] = f; +``` + +那么怎么做呢? + +## 2.支持结构化绑定 + +这个问题我们使用两个步骤: + +1.特化 `std::tuple_size` 和 `std::tuple_element` + +```cpp +namespace std { + template <> + struct tuple_size : std::integral_constant {}; + + template <> + struct tuple_element<0, Foo> { + using type = int64_t; + }; + + template <> + struct tuple_element<1, Foo> { + using type = std::string; + }; +} +``` + +2.为上面的结构体添加友元get函数,然后实现即可: + +```cpp +template +decltype(auto) get(const Foo& f); + +template <> +decltype(auto) get<0>(const Foo& f) { + return f.id_; +} + +template <> +decltype(auto) get<1>(const Foo& f) { + return f.data_; +} +``` + +此时便可以正常的工作了,得到输出: + +```cpp +id = 12, data = hello world +id = 12, data = hello world +``` + +最后,抛出几个问题: + +- 上面提供的是只读访问,如果在访问的时候修改const auto &会发生什么? + +例如: + +```cpp +auto& [id, data] = f; +``` + +- 如何支持对类成员的修改访问? + +即,让这个语句能够正常运行 + +```cpp +auto& [id, data] = f; +``` + +好了,本节完,欢迎留言区交流。 \ No newline at end of file diff --git a/site_extras/modern-cpp-cpp17-template-auto-deduction.md b/site_extras/modern-cpp-cpp17-template-auto-deduction.md new file mode 100644 index 0000000..d2c3c3d --- /dev/null +++ b/site_extras/modern-cpp-cpp17-template-auto-deduction.md @@ -0,0 +1,74 @@ +> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp17/template-auto-deduction/) 的文章,由站点同步脚本导出。 + +# C++17 模板 auto 推导 + +## C++之万物皆可auto + +C++17引入了许多旨在提高代码表达性并减少模板代码冗余的功能。其中一个显著的增强是使用`auto`关键字声明非类型模板参数的能力。在某些场景,这个特性特别方便,因为它消除了显式类型声明的需要。以下我将会引入我在工作中,也就是数据库当中如何使用这一特性! + +从实践中学习,本节的完整源码会上传至星球,感兴趣戳尾页二维码。 + +## 在C++17中使用`auto`的非类型模板参数 + +### 在数据库场景中进行排序 + +考虑一个与数据库相关的类`Foo`,你希望根据枚举表示的特定顺序进行排序: + +```cpp +enum class SortOrder { Ascending, Descending }; +``` + +你有不同排序顺序的`Foo`实例: + +```cpp +Foo f1; +Foo f2; +``` + +### 以前的实现方式 + +在C++17之前,我们可能会这样实现: + +```cpp +template class T, U K> +auto GetSortOrder(const T&) { + return K; +} +``` + +然而,这可能会导致错误,因为编译器无法推断模板参数'U'。 + +### 现在的简化方式 + +在C++17之后,对于非类型模板参数,我们可以使用`auto`关键字来取代: + +```cpp +template