系列文章目录
前言
C++编码方法
一、内存管理
一种智能指针的可能实现:
https://blog.csdn.net/surfaceyan/article/details/139887490
unique ptr deleter
int* my_alloc(int val) { return new int(val); }
void my_free(int* p) { delete p; }
std::unique_ptr<int, decltype(&my_free)> up { my_alloc(5), &my_free} ;
enable_shared_from_this
class Foo : public enable_shared_from_this<Foo>
{
public:
shared_ptr<Foo> get_pointer() {
return shared_from_this();
}
};
auto ptr1 { make_shared<Foo>() };
auto ptr2 { ptr1->get_pointer() };
二、类与对象
对象构造与析构: https://blog.csdn.net/surfaceyan/article/details/151157610
// 这个一个名为spreadsheet_cell的模块的定义
export module spreadsheet_cell;
// 如果该类是在模块中定义的,并且该类应该对带入该模块的客户可见则class前加export关键字
export class SpreadsheetCell {
public:
// 成员可以是 变量、函数、枚举、类型别名、嵌套类等
};
模块定义文件:
// 指定实现的方法用于哪个模块
module spreadsheet_cell;
void SpreadsheetCell::set_value()
{
;
}
c++23显示对象参数
void SpreadsheetCell::set_value(this SpreadsheetCell& self, double value)
{
self.value_ = value;
print_cell(self);
}
SpreadsheetCell my_cell;
my_cell.set_value(6.6); // 和隐式this的用法一样,即使...
第一个参数现在是显示对象参数,通常为self,可使用任意变量名。self的类型以this关键字作为前缀,此显示对象参数必须是成员函数的第一个参数,一旦使用显示对象参数,函数就不在隐式定义this,因此在set_value函数体中,必须显示的使用self来访问类中的任何内容。
-
构造函数
没有参数的构造函数称为默认构造函数 / 无参构造函数SpreadsheetCell::SpreadsheetCell(string_view init_value) { // 显示调用SpreadsheetCell构造函数实际上新建了一个SpreadsheetCell类型的临时未命名对象 SpreadsheetCell(string_to_double(init_value)); }如果没有指定任何构造函数,编译器将自动生成无参构造函数,类的所有对象成员都可以调用编译器生成的默认构造函数,但不会初始化语言的基本类型。如果声明了任何构造函数,编译器就不会再自动生成默认构造函数
如果类的数据成员具有删除的默认构造函数,则该类的默认构造函数也会自动删除
构造函数初始化器 或 成员初始化列表,允许在创建数据成员时执行初始化 -
拷贝构造函数
export class SpreadsheetCell { public: SpreadsheetCell(const SpreadsheetCell& src); };如果没有编写拷贝构造函数,C++会自动生成一个,用源对象中响应的数据成员的值初始化新对象的每个数据成员,如果数据成员是对象,初始化意味着调用它们的拷贝构造函数。
如果类的数据成员具有删除的或私有的拷贝构造函数,则该类的拷贝构造函数也会自动删除,即使你显示默认了一个explicit MyClass(int); explicit MyClass(int, int); process({1}); // compile error process({1,2}); // compile error -
析构
-
拷贝赋值运算符
如果没有编写自己的赋值运算符,C++将自动生成一个:以递归方式用源对象的每个数据成员并赋值给目标对象
复制和交换是一种惯用方法
- 移动构造函数
C++中左值是可以获取其地址的一个量;所有不是左值的量都是右值,如字面量、临时对象或值。
右值引用是一个对右值的引用,这是一个当右值是临时对象或使用std::move()显式移动时才适用的概念。通常,临时对象被当做const T&,但当函数重载使用了右值引用时,可以解析临时对象,用于函数重载。
临时值的生命周期只是当前行,即,当前行执行完毕,则临时值生命周期结束,销毁该临时对象。当临时对象被右值引用时,临时对象的生命周期和该右值引用变量绑定。template <typename T> struct MyVec: public std::vector<T> { MyVec() { printf("default\n"); } MyVec(const MyVec&) { printf("copy ctor\n"); } // MyVec(MyVec&&) // { // printf("move ctor\n"); // } }; // 当定义出移动构造时, a对象由移动构造函数生成 // 当没有定义移动构造时,a对象由拷贝构造函数生成 // std::move就是进行类型强制转换 static_cast<T&&>(value) // 临时对象或者右值的数据类型为 T&& MyVec<int> a(std::move(ret_testa())); // 合法 MyVec<int>&& r_ref = std::move(ret_testa()); // 合法 const MyVec<int>& l_const_ref = std::move(ret_testa()); - 移动赋值运算符
Spreadsheet(Spreadsheet&& src) noexcept;
Spreadsheet& operator=(Spreadsheet&& rhs) noexcept;
MyVec<int> ret_testa()
{
int a = 45;
return MyVec<int>(); // 实测这里只有一次默认构造
return std::move( MyVec<int>() ); // 一次默认构造,一次移动构造
}
三、继承
类型转换
5种特定的强制类型转换:
const_cast() static_cast() reinterpret_cast() dynamic_cast() std::bit_cast()
C风格的强制类型转换涵盖了所有4种C++强制类型转换,因此它的意图不总是那么明显。
const_cast()
将const类型转为非const类型
只能是指针或者引用
const int a{4};
int* p = const_cast<int*>(&a);
// const int* p2 = static_cast<int*>(&a); ERROR
const int& ra{a};
int& rp = const_cast<int&>(ra);
// int& rp2 = static_cast<int&>(ra); ERROR
// 下面也是可以的,不过 const_cast 常用于将const类型转为非const类型
int b{90};
const_cast<const int*>(&b); // OK
static_cast<const int*>(&b); // OK
static_cast()
执行语言直接支持的显示转换。
int i{3};
int j{4};
double result{static_cast<double>(i) / j};
使用static_cast()执行现实转换,如果类A有一个接受类B对象的构造函数,则可以使用static_cast()将类B对象转换为A对象。但是,在大多数情况下,需要这种行为时,编译器会自动执行转换。
reinterpret_cast()
可将一种类型的指针或引用强制转换为另一种类型的指针或引用。
class X{}; class Y{};
X x; Y y;
X* xp{&x};
Y* yp{&y};
// 必须为reinterpret_cast。stati_cast报编译错误
xp = reinterpret_cast<X*>(yp);
// 隐式转换
void* p{xp};
// 用static_cast就足够了
xp = static_cast<X*>(p);
// 必须为reinterpret_cast。stati_cast报编译错误
X& xr{x};
Y& yr{reinterpret_cast<Y&>(x)};
dynamic_cast()
它提供了对继承层次结构中的强制转换的运行时检查。可用于强制转换指针或引用。dynamic_cast()在运行时检查底层对象的运行时类型信息。如果强制转换无效,dynamic_cast()将返回空指针(对于指针),或抛出std::bad_cast异常(对于引用)。
dynamic_cast必须用于多态类型,否则会编译报错。
与static_cast和reinterpret_cast相比,dynamic_cast提供运行时(动态)类型检查,其它两个则执行强制转换,即使转换的类型是错误的。
std::bit_cast()
它定义在<bit>头文件中,它是标准库中唯一的一种转换方式,而其它转换方式是C++语言本身的一部分。
float as_float{1.23f};
auto as_uint{std::bit_cast<unsigned int>(as_float)};
std::bit_cast<float>(as_uint) == as_float
类型转换小结
| 场景 | 类型转换 |
|---|---|
| 移除const属性 | const_cast |
| 语言支持的显式强制转换(int->double) | static_cast |
| 用户定义的构造函数或转换函数支持的显式强制转换 | static_cast |
| 一个类的对象转换为另一个(无关的)类对象 | std::bit_cast |
| 在同一继承层次结构中,一个类的指针或引用转为另一个类的指针或引用 | 建议dynamic_cast(执行动态检查),或static_cast(不执行动态检查) |
| 指向一种类型的指针或引用转为指向其它不相关类型的指针或引用 | reinterpret_cast |
| 指向函数的指针转换为指向函数的指针 | ??? |
四、模块
模块
// 可以访问c++标准库中的所有内容
import std;
模块接口文件
模块接口文件通常以.cppm作为扩展名。模块接口以声明开头,声明该文件正在定义一个具有特定名称的模块。这称为模块声明。模块的名称可以是任何有效的C++标识符,名称中可以包含(.)如:datamodel、mycomp.datamodel、mycomp.datamodel.core
模块需要显示的声明要导出的内容,即当用户代码导入模块时,哪些内容应该是可见的。使用export关键字从模块中导出实体(类、函数、常量、其它模块等)。任何没有从模块中导出的内容,只在该模块中可见。所有导出实体的集合称为模块接口。
// 一个名为Person.cppm的模块接口文件,定义person模块并导出person类
export module person; // 命名模块声明,该模块的名称为person
import std;
export class Person
{
public:
...
private:
...
};
在标准术语中,从命名模块声明开始直到文件末尾的所有内容都被称为模块范围。
/// 导出命名空间,则该命名空间中的左右内容也将自动导出
export namespace DataModel
{
class Person {};
class Address {};
using Persons = std::vector<Person>;
}
// 导出块
export {
namespace DataModel {
class Person {};
class Address {};
using Persons = std::vector<Person>;
}
}
使用:
import person;
import std;
using namespace std;
int main() {
Person person;
println("{}", person);
}
模块实现文件
// Person.cpp
module person;
using namespace std;
Person::Person() {}
...
实现文件没有person模块的导入声明。module person的声明隐式包含import person的声明。
预处理指令
https://blog.csdn.net/surfaceyan/article/details/139940255?spm=1011.2415.3001.5331
| 预处理指令 | 功能 |
|---|---|
| #include [file] | 将名为[file]的文件内容插入到指令位置 |
| #define [id] [value] | 将id替换为value |
| #undef | 取消先前的define |
| #if [expression] #elif [expression] #else #endif | |
| #ifdef [id] #endif #ifndef [id] #endif | |
| #pragma [xyz] | 控制编译器特定行为 |
| #error [message] | 停止编译 |

查询头文件是否存在
#if __has_include(<optional>)
#include <optional>
#elif __has_include(<experimental/optional>)
#include <experimental/optional>
#endif
语言特性测试宏
__cpp_range_based_for
__cpp_binary_literals
__cpp_char8_t
__cpp_generic_lambdas
__cpp_consteval
__cpp_coroutines
...
__has_cpp_atribute([attribute_name])
...
static关键字的作用
- 类的静态数据成员 和 静态成员函数
- 函数中的静态变量
(在不同的源文件中,非局部变量的初始化顺序是不确定的)
C风格的可变长度参数列表
#include <stdio.h>
#include <stdarg.h>
void debug(const char* str, ...) {
va_list ap;
va_start(ap, str);
vfprintf(stderr, str, ap);
va_end(ap);
}
使用stdarg.h中定义的宏va_list(), va_start(), va_end()。debug函数的原型包含一个具有类型和名称的参数str,之后的省略号(…)代表任意数量和类型的参数。如果要访问这些参数,必须声明一个va_list类型的变量,并使用va_start()调用初始化它。va_start()的第二个参数必须是参数列表中最右边的已命名变量。所有具有变长参数列表的函数都至少需要一个已命名的参数。debug函数只是将列表传递给vfprintf()(stdio.h中的标准函数)。vfprintf的调用返回时,debug调用va_end()来终止对边长参数列表的访问。在调用va_start()之后,必须总是调用va_end(),以确保函数结束后,栈处于稳定的状态。
形如(const cahr* str, ...)的可变形参列表存在两个问题:
- 无法知道参数的个数
- 无法知道参数的类型
因此必须有一个约定知道上面的信息,printf()函数通过解析%s, %d等得到,下面介绍另一种简单的方法:
// 函数的第一个参数指明参数的个数,参数类型约定为int型
void PrintInts(int num, ...) {
va_list ap;
va_start(ap, num);
for (int i = 0; i < num; ++i) {
int tmp = va_arg(ap, int);
std::print("{} ", tmp);
}
va_end(ap);
std::println("");
}
五、模板与泛型
概念
c++20引入了概念(concept),一种用来约束类模板和函数模板的模板类型和非类型参数的命名要求。,概念允许编译器在不满足某些类型约束时输出可读的错误信息。
概念永远不会被实例化。
template <parameter-list>
concept concept-name = constraints-expression;
template <typename T>
concept Big = sizeof(T) > 4;
计算结果为bool值的常量表达式(即任何可以在编译时求值的表达式),可以直接用作概念定义的约束。
requires表达式
requires (parameter-list) { requirements; }
requires 是一系列要求,每个要求都必须以分号结尾
简单requirement
template<typename T> // 不允许使用变量声明、循环、条件语句,这个表达式语句永远不会被计算;
// 编译器只是用于验证它是否可通过编译
concept Incrementable = requires(T x) { x++; ++x; };
类型requirement
类型requirement用于验证特定类型是否有效。下面的概念要求特定类型T有value_type成员
template <typename T>
concept C = requires { typename T::value_type; };
// 验证某个模板是否可以使用给定的类型进行实例化
template <typename T>
concept C = requires { typename SomeTemplate<T>; };
复合requirement
复合requirement可以用于验证某些东不会抛出任何异常和/或验证某个成员函数是否返回某个类型
// 语法
{ expression } noexcept -> type-constraint;
// T类型是否具有标记为noexcept的析构函数和swap()成员函数
template <typename T>
concept C = requires (T x, T y) {
{ x.~T() } noexcept;
{ x.swap(y) } noexcept;
};
type-constraint必须是类型约束,不能是具体的类型
下面概念验证了给定类型具有一个名为size()的成员函数,该成员函数的返回类型可转换为size_t类型
template <typename T>
concept C = requires(const T x) {
{ x.size() } -> convertible_to<size_t>;
};
std::convertible_to<From, To> 是标准库在<concepts>中预定义的概念,他有两个模板类型参数。箭头左边的表达式的类型自动作为第一个模板类型参数传递给convertible_to的类型约束。因此,只需指定To模板类型实参(本例size_t)
// 要求T类型是可比较的
template <typename T>
concept Comparable = requires(const T a, const T b) {
{ a == b } -> convertible_to<bool>;
{ a < b } -> convertible_to<bool>;
};
嵌套requirement
template <typename T>
concept C = requires(T t) {
++t; --t; t++; t--;
requires sizeof(t) == 4;
};
组合概念表达式
template <typename T>
concept IncAndDec = Incrementable<T> && Decrementable<T>;
- 核心语言概念:
same_as, derived_from, convertible_to, integral, floating_point, copy_constructible - 比较概念:
equality_comparable, totaly_ordered - 对象概念:
movable, copyable - 可调用概念:
invocable, predicate
类型约束的auto
Incrementable auto value{1};
Incrementable auto value{"abc"s}; // error 不满足概念约束
使用
template <typanem T> requires constraints-expression(可以是任何值为bool类型的常量表达式)
void process(const T& t);
template <convertible_to<bool> T>
void process(const T& t);
完全类似于:
template <typename T> requires convertible_to<T, bool>
void process(const T& t);
void process(const Incrementable auto& t);
六、错误处理
程序运行不可避免的会遇到错误,如请求系统调用失败,此时该失败可能会以异常的形式抛出。该异常会被处理异常的代码捕获,异常执行流和征程代码的执行流不同。
C程序中有 setjmp()/longjmp() 机制,但是这种机制会绕开C++中的栈栈中的作用域析构函数。
在设计和编写程序时必须有错误处理方案
捕获和抛出异常
#include <stdexcept>
double safeDivide(double num, double den)
{
if (den == 0) throw std::invalid_argument{ "Divide by zero" };
return num / den;
}
try {
safeDivide(110, 0);
} catch (const std::invalid_argument& e) {
println("Caught exception: {}", e.what());
}
抛出 int 型数和 const char*
try {
throw 5;
} catch (int e) {
println("error code: {}", e);
}
try {
throw "unable to xxx";
} catch (const char* e) {
println("{}", e);
}
未捕获的异常
当一个异常未被捕获时,程序的行为大概如下:
try {
main(argc, argv);
} catch (...) {
if (terminate_handler != nullptr) {
terminate_handler();
} else {
std::terminate();
}
}
std::terminate()函数的默认行为是调用abort()终止进程,可以通过set_terminate修改其行为
[[noreturn]] void MyTerminate()
{
println(cerr, "Uncaught exception!");
_Exit(1);
}
int main()
{
std::set_terminate(&MyTerminate);
throw;
}
| api | 来源 | 行为 |
|---|---|---|
| _exit | posix | 直接退出程序,没有任何清理动作 |
| _Exit | libc | 对posix的封装,直接退出程序,没有任何清理动作 |
| exit | libc | 执行清理,然后退出 |
| abort | libc | 1. 向当前进程发送 SIGABRT 信号 2. 如果 SIGABRT 的信号处理函数执行完毕且没有终止进程,系统会再次强制触发 SIGABRT(二次兜底) 3. 最终终止进程并生成 core dump |
noexcept
如果一个函数带有noexcept标记,却以某种方式抛出了异常,C++将调用terminate()来终止应用程序。
派生类中的虚成员函数可标记未noexcept(即使基类中的版本不是noexcept),反之不行。
noexcept(expression)说明符
仅当给定的表达式为true时,才标记为noexcept
noexcept(expression)运算符
如果给定的表达式标记为noexcept,那么noexcept(expression)运算符会返回true。这会在编译期求值。
异常与多态
在类层次结构中捕获异常
异常层次结构的一个特性是可利用多态性捕获异常。
作为异常抛出的对象至少要复制或移动一次。
异常可能被复制多次,但只有按值(而不是按引用)捕获异常才如此
按引用(最好是const引用)捕获异常对象可避免不必要的复制。
C++ 标准规定,作为异常抛出的对象,其类型必须是可复制构造或可移动构造的。 即使由于编译器的优化(如 NRVO 具名返回值优化或 C++17 强制的拷贝消除),实际运行中可能连一次拷贝/移动都没有发生(直接在异常专有内存区构造该对象),但语法上仍要求该类必须具备公有且未被删除的复制/移动构造函数。
总结: 因为异常对象的生命周期必须超越抛出它的局部作用域(存活到被 catch 块处理完毕),所以它必须被复制或移动到由 C++ 运行时管理的特殊内存区域中。(所以异常对象因该是被分配到堆上,生命周期由分配该对象的管理,除非调用current_exception)
嵌套异常
std::throw_with_nested() std::nested_exception, std::rethrow_if_nested(), current_exception(), exception_ptr
嵌套异常的概念
在复杂的系统中,底层的模块可能会抛出底层异常(例如“文件未找到”)。当高层模块捕获到这个异常时,可能希望抛出一个更有业务语义的高层异常(例如“系统初始化失败”)。如果直接抛出新异常,底层的原始错误信息就会丢失。
为了解决这个问题,C++11 引入了嵌套异常。你可以使用 std::throw_with_nested 将一个新异常和当前正在处理的原始异常(通过 std::current_exception 获取)打包在一起抛出。在最上层捕获异常时,不仅能得到高层异常的信息,还能通过 std::rethrow_if_nested 递归地解包并获取下层原始异常的信息。
void DoSome() {
try {
throw std::runtime_error("runtime error");
} catch(const std::exception& e) {
// throw_with_nested()抛出一个未命名的编译器生成的新类型,这个类型由nested_exception和例子中的MyException派生而来
// throw_with_nested是个函数模板,内部 throw 类似下面的类
std::throw_with_nested(std::logic_error("logic error"));
/*
* template<typename _Except>
* struct _Nested_exception : public _Except, public nested_exception
* { };
*
*/
}
}
int main() {
try {
DoSome();
} catch(const std::exception& e) {
std::cerr << e.what() << '\n';
const auto* nested { dynamic_cast<const std::nested_exception*>(&e) };
if (nested) {
try{
nested->rethrow_nested();
} catch(const std::exception& e) {
std::cerr << e.what() << std::endl;
}
}
}
}
// 用std::rethrow_if_nested()去掉dynamic_case()
try {
DoSome();
} catch(const std::exception& e) {
std::cerr << e.what() << '\n';
try {
std::rethrow_if_nested(e);
} catch(const std::exception& e) {
std::cerr << e.what() << '\n';
}
}
nested_exception 基类的默认构造函数通过调用std::current_exception()捕获正在处理的异常,并将其存储在std::exception_ptr中。
current_exception() 的作用:获取当前正在 catch 块中处理的异常对象的指针(捕获当前正在处理的异常),并安全地延长它的生命周期,以便在脱离当前的 catch 块之后(甚至在其他线程中)保留并重新抛出该异常。
class nested_exception {
exception_ptr _M_ptr;
public:
nested_exception() noexcept : _M_ptr(current_exception()) { }
nested_exception(const nested_exception&) noexcept = default;
nested_exception& operator=(const nested_exception&) noexcept = default;
virtual ~nested_exception() noexcept;
[[noreturn]] void rethrow_nested() const {
// rethrow_exception是个函数,Throw the object pointed to by the exception_ptr.
if (_M_ptr) rethrow_exception(_M_ptr);
std::terminate();
}
exception_ptr nested_ptr() const noexcept
{ return _M_ptr; }
};
递归打印多层嵌套异常
// 递归打印异常信息的辅助函数
void print_nested_exception(const std::exception& e, int level = 0) {
std::cerr << std::string(level, ' ') << "Exception: " << e.what() << '\n';
try {
// 如果异常中嵌套了底层异常,则将其重新抛出以便在下面的 catch 块中捕获
std::rethrow_if_nested(e);
} catch (const std::exception& nested) {
print_nested_exception(nested, level + 2);
} catch (...) {
// 捕获非派生自 std::exception 的异常
}
}
重新抛出异常
try {
throw std::runtime_error("runtime error");
} catch (const std::exception& e) {
throw; // rethrow 重新抛出最近捕获的异常
// throw e; 会执行截断操作!!!
}
如果构造函数抛出了异常,则对应的析构函数不会被调用
只有完全构造完的对象才会调用析构函数。已经构造完成的基类和成员变量会调用其对应的析构函数。
析构函数不应该抛出任何异常
堆栈跟踪
C++23
<stacktrace>
获取当前的堆栈跟踪 std::stacktrace::current(),遍历该堆栈跟踪中的每个帧。一个帧由std::stacktrace_entry类表示,stacktrace_entry类支持:
- description(): 返回帧的描述
- source_file(), source_line()
- C++标准库
在已发布的文章中搜索: C++ STL
- 5种顺序容器:vector, list, forward_list, deque, array
- 顺序视图: span,mdspan(c++23多维试图)
- 3个非关联容器适配器:queue, priority_queue, stack
- 有序关联容器:set, multiset, map, multimap
- 无序关联容器:unordered_map, unordered_multimap, unordered_set, unordered_multiset
- 平坦关联容器适配器:flat_map, flat_multimap, flat_set, flat_multiset
- bitset
迭代器
迭代器萃取器,迭代器信息
iterator_traits<IteratorType>::value_type;
iterator_traits<IteratorType>::pointer;
iterator_traits<IteratorType>::reference;
iterator_traits<IteratorType>::iterator_category
iterator_traits<IteratorType>::difference_type;
输出流迭代器:ostream_iterator
输入流迭代器:istream_iterator
输入流迭代器:istreambuf_iterator
迭代器适配器
back_insert_iterator: 使用push_back将元素插入容器中
front_insert_iterator: 使用push_front将元素插入容器中
insert_iterator: 使用insert将元素插入容器中
reverse_iterator: 反转一个迭代器的迭代顺序
move_iterator:
ranges
范围库提供的范围是迭代器之上的抽象层,消除了不匹配的迭代器错误,并添加了额外功能。
vector data{3, 2, 1};
sort(begin(data), end(data));
// 两个迭代器是不是太不方便了?
=》
ranges::sort(data);
view
std::ranges::views::xxx
vector data{1,2,3,4,5,6,7,8,9,10};
auto result { data
| views::filter([](const auto& value){ return value % 2 == 0; })
| views::transform([](const auto& value){ return value * 2.0; })
| views::drop(2)
| views::reverse
};
auto res { data
| views::filter([](const auto& v){ return v % 2 == 0; })
| views::take(3)
| views::transform([](const auto& v){ return format("{}", v); })
};
范围工厂
| 范围工厂 | 描述 |
|---|---|
| empty_view | 创建一个空视图 |
| single_view | 创建具有单个给定元素的视图 |
| iota_view | 创建一个无限或有界的视图,其中包含以初始值开始的元素,每个后续元素的值等于前一个元素的值加1 |
| repeat_view | 创建一个视图,该视图重复给定的值。生成的视图可以是无界的,也可以由给定的值数量限制 |
| basic_istream_view istream_view | 创建一个视图,其中包含通过调用底层输入流上的调用提取运算符(>>)检索到的元素 |
ranges::iota_view iota(1000003);
for (auto i : iota | ranges::views::filter([](int i){ return i % 2 == 0; })) {
println("i: {}", i);
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
println("repeat view: {:n}", std::views::repeat("hello", 5));
println("repeat view: {:n}", std::ranges::views::repeat("hello", 5));
println("repeat view: {:n}", std::ranges::repeat_view("hello", 5));
println("Type integers, an integer >=5 stops the program.");
for (auto v : ranges::istream_view<int>{cin}
| views::take_while([](const auto& v){ return v < 5; })
| views::transform([](const auto& v){ return v * 2; })){
print("> {} ", v);
}
println("Terminating...");
范围与容器的互操作
顺序视图
void print(std::span<int> s) {
for (auto& i : s) println("{}", i);
}
vector<int> v{1, 2, 3, 4, 5};
int arr[7] = {1, 2, 3, 4, 6,7, 8};
print(arr);
// print(v);
关联容器
节点
所有有序和无序的关联容器都被成为基于节点的数据结构。准库以节点句柄的形式提供对节点的直接访问。确切类型并未指定,但每个容器都有一个名为node_type的类型别名,它指定容器节点句柄的类型。节点句柄只能移动,是节点中存储的元素的所有者。它提供了对键和值的rw访问。
从关联容器使用extract()提取节点时,将会把它从节点中删除,因为返回的节点句柄时所提取元素的唯一拥有者。
C++提供了新的insert重载,以允许从容器中插入节点句柄。
使用extract提取节点句柄和insert插入,可以将数据从关联容器移入另一个而不需要执行任何复制或移动操作。
map<int, Data> datamap2;
auto node{datamap.extract(1)};
datamap2.insert(move(node));
datamap2.insert(datamap.extract(1));
// merge() 操作可将所有节点从一个关联容器移到另一个中。
// 无法移动的则留在原容器中,因为可能导致目标容器重复。
datamap2.merge(datamap);
无序关联容器/哈希表
无序关联容器也称为哈希表,这是因为它们使用了哈希函数(hash function)。哈希表的实现通常会使用某种形式的数组,数组中的每个元素都称为桶(bucket)。每个桶都有一个特定的数值索引,例如0、1、2直到最后一个桶。哈希函数将键转换为哈希值,再转换为桶索引,与这个键关联的值在桶中存储。
哈希函数的结果未必是唯一的。两个或多个键哈希到同一个桶索引,就称为冲突/哈希碰撞。当使用不同的键得到相同的哈希值,或把不同的哈希值转换为同一桶索引时,就会发生冲突。可用二次重哈希或线性链等方法解决。
namespace {
template<> struct hash<IntWrapper> {
size_t operator()(const IntWrapper& x) const {
return std::hash<int>{}(x.getValue());
}
};
}
template <typename Key,
typename T,
typename Hash = hash<Key>,
typename Pred = std::equal_to<Key>,
typename Alloc = std::allocator<std::pair<const Key, T>>
class unordered_map;
平坦集合和平坦映射关联容器适配器
C++23引入新的容器适配器
- std::flat_set, std::flat_multiset
- std::flat_map, std::flat_multimap
bitset
bitset<10> bs;
bs.set(3);
bs.set(6);
bs[8] = true;
bs[9] = bs[3];
bs.to_string(); // 1101001000
函数指针
指向成员函数(和数据成员)的指针
int (Employee::*functionPtr)()const { &Employee::GetSalary };
Employee emp {"John", "Doe"};
(emp.*functionPtr)();
Employee* emp2 = &emp;
(emp2->*functionPtr)();
using PtrToGet = int (Employee::*) () const;
PtrToGet funcPtr {&Employee::GetSalary};
auto funcPtr {&Employee::GetSalary};
(emp2->*funcPtr)();
透明运算符仿函数
使用透明运算符而不是非透明运算符可以提高性能
accumulate(cbegin(values), cend(values), 1.1, multiplies<>{});
priority_queue<int, vector<int>, greater<>> q;
适配器函数对象
模板在推导类型时会丢失引用属性(非T&&的情况下)
std::bind()
std::ref和std::cref可用于绑定非const引用和const引用
其返回值为 std::reference_wrapper<T>
void inc(int& idx) { ++idx; }
int index {0};
inc(index); // index的值变为1
auto incr{std::bind(inc, index)};
incr(); // index的值不变,因为生成的是index的拷贝,这个拷贝的引用被绑定到inc函数的第一个参数中
auto incr{std::bind(inc, std::ref(index))};
标准库算法
算法与容器关系图
概览
find
template<typename _InputIterator, typename _Tp>
inline _InputIterator find(_InputIterator __first, _InputIterator __last, const _Tp& __val);
if (auto it { find(cbegin(v), cend(v), value); }; it == cend(v)) {
;
} else {
;
}
find_if
template<typename _InputIterator, typename _Predicate>
inline _InputIterator
find_if(_InputIterator __first, _InputIterator __last,_Predicate __pred);
大部分算法定义在algorithm头文件中,少数定义在numeric中
略
时间和日期工具
- 编译期有理数的使用
- 时间的使用
- 日期和日历的使用
- 如果转换不同时区的时间点
编译期有理数 ratio
可通过ratio库精确的表示任何可在编译期使用的有限有理数。
ratio是一个类模板,ratio类模板的特定实例表示一个特定的有理数。要命名这种特定的实例可以使用类型别名。它的算数运算和逻辑运算都是针对模板实例化后的类的,而非C++运行时的对象的。
// 定义一个表示1/60的有理数编译期常量
using r1 = std::ratio<1, 60>
// r1有理数的分子(num)和分母(den)是编译期常量,可如下获取:
intmax_t num { r1::num };
intmax_t den { r1::den };
ratio算数类模板:ratio_add, ratio_subtract, ratio_multiply, ratio_divide
// add two rational numbers
using r2 = ratio<1, 30>;
using result = std::ratio_add<r1, r2>::type;
println("sum = {}/{}", result::num, result::den);
ratio逻辑运算模板:ratio_equal, ratio_not_equal, ratio_less, ratio_less_equal, ratio_greater, ratio_greater_equal
逻辑运算的结果用一种新类型std::bool_constant表示
/// integral_constant
template<typename _Tp, _Tp __v>
struct integral_constant;
template<bool __v>
using bool_constant = integral_constant<bool, __v>;
using res = ratio_less<r1, r2>;
println("{}", res::value);
SI类型别名
typedef ratio<1, 1000000000000000000> atto;
typedef ratio<1, 1000000000000000> femto;
typedef ratio<1, 1000000000000> pico;
typedef ratio<1, 1000000000> nano;
typedef ratio<1, 1000000> micro;
typedef ratio<1, 1000> milli;
typedef ratio<1, 100> centi;
typedef ratio<1, 10> deci;
typedef ratio< 10, 1> deca;
typedef ratio< 100, 1> hecto;
typedef ratio< 1000, 1> kilo;
typedef ratio< 1000000, 1> mega;
typedef ratio< 1000000000, 1> giga;
typedef ratio< 1000000000000, 1> tera;
typedef ratio< 1000000000000000, 1> peta;
typedef ratio< 1000000000000000000, 1> exa;
持续时间 duration
duration表示的是两个time_point之间的间隔时间。duration保存了滴答数和滴答周期(tick period),滴答周期是两个滴答之间的秒数,是一个编译期ratio常量,可以是1秒的分数。
/// `chrono::duration` represents a distance between two points in time
template<typename _Rep, typename _Period = ratio<1>>
struct duration;
_Rep 表示滴答数的变量类型,应该是一种算数类型,如long, double等。_Period表示滴答周期的有理数常量,默认滴答周期为1秒。
duration模板类的方法
| 成员函数 | 说明 |
|---|---|
| _Rep count() const. | 返回滴答数值 |
| static duration zero() | 返回持续时间为0的duration |
| static duration min() static duration max() | 返回duration模板指定的类型参数表示的最小/最大持续时间的duration值 |
// 滴答周期为1s的duration
duration<long> d1;
// 滴答周期60s
duration<long, ratio<60>> d2;
// 滴答周期为60s
duration<double, ratio<1, 60>> d3;
// 滴答周期为1ms
duration<long long, milli> d4;
// specify a duration where each tick is 60 seconds
std::chrono::duration<long, std::ratio<60>> d1{123};
std::println("{} ({})", d1, d1.count()); // 123min (123)
auto d2 { std::chrono::duration<double>::max() };
std::println("{}", d2);
std::chrono::duration<long, std::ratio<60>> d3{10}; // 10 min
std::chrono::duration<long, std::ratio<1>> d4{14}; // 14 sec
if (d3 > d4) { std::println("d3 > d4"); }
else { std::println("d3 <= d4"); }
++d4; // +1 sec resulting 15 sec
d4 *= 2; // resulting 30 sec
std::chrono::duration<double, std::ratio<60>> d5{d3+d4}; // 相加并转换格式
std::chrono::duration<long, std::ratio<1>> d6 {d3 + d4};
std::println("{} + {} = {} or {}", d3, d4, d5, d6);
std::chrono::duration<long> d7{30}; // 30 sec
std::chrono::duration<double, std::ratio<60>> d8 {d7}; // 转为分钟
std::println("{} = {}", d7, d8);
std::println("{} seconds = {} minutes", d7.count(), d8.count());
duration<long> d7{30}; // 30 sec
// 强制转换为long 分钟
auto d8 { duration_cast< duration<long, ratio<60>> >(d7) };
预定义的duration 在std::chrono命名空间中
/// nanoseconds
using nanoseconds = duration<_GLIBCXX_CHRONO_INT64_T, nano>;
/// microseconds
using microseconds = duration<_GLIBCXX_CHRONO_INT64_T, micro>;
/// milliseconds
using milliseconds = duration<_GLIBCXX_CHRONO_INT64_T, milli>;
/// seconds
using seconds = duration<_GLIBCXX_CHRONO_INT64_T>;
/// minutes
using minutes = duration<_GLIBCXX_CHRONO_INT64_T, ratio< 60>>;
/// hours
using hours = duration<_GLIBCXX_CHRONO_INT64_T, ratio<3600>>;
#if __cplusplus > 201703L
/// days
using days = duration<_GLIBCXX_CHRONO_INT64_T, ratio<86400>>;
/// weeks
using weeks = duration<_GLIBCXX_CHRONO_INT64_T, ratio<604800>>;
/// years
using years = duration<_GLIBCXX_CHRONO_INT64_T, ratio<31556952>>;
/// months
using months = duration<_GLIBCXX_CHRONO_INT64_T, ratio<2629746>>;
#endif // C++20
使用
C++标准要求预定义的duration使用整数类型,如果转换后出现非整数值则会报编译错误。
using namespace std::chrono;
auto t { hours{1} + minutes{23} + seconds{45} };
std::println("{}", seconds{t});
// 编译失败,因为90秒转换后为1.5分钟,不是整数
std::chrono::seconds s {90};
std::chrono::minutes m {s};
// 这也无法编译,因为从秒到分钟的转换可能产生非整数值
std::chrono::seconds s {60};
std::chrono::minutes m {s};
// 另一个方向,从分钟转换成秒总是能成功
标准字面量
h, min, s, ms, us, ns创建duration,可使用下面任意一个using指令来使用chrono字面量
using namespace std;
using namespace std::literals;
using namespace std::literals::chrono_literals;
using namespace std::chrono_literals;
时钟 clock
时钟的epoch是时钟开始计时的时间system_clock的纪元从1970-01-01 00:00:00
namespace chrono {
...
struct steady_clock
{
typedef chrono::nanoseconds duration;
typedef duration::rep rep;
typedef duration::period period;
typedef chrono::time_point<steady_clock, duration> time_point;
static constexpr bool is_steady = true;
static time_point now() noexcept;
};
struct system_clock
{
typedef chrono::nanoseconds duration;
typedef duration::rep rep;
typedef duration::period period;
typedef chrono::time_point<system_clock, duration> time_point;
static_assert(system_clock::duration::min()
< system_clock::duration::zero(),
// 标准库这里应该是写错了,应该是must
"a clock's minimum duration cannot be less than its epoch");
static constexpr bool is_steady = false;
static time_point now() noexcept;
// Map to C API
static std::time_t to_time_t(const time_point& __t) noexcept
{ return std::time_t( duration_cast<chrono::seconds>(__t.time_since_epoch()) .count() ); }
static time_point from_time_t(std::time_t __t) noexcept
{
typedef chrono::time_point<system_clock, seconds> __from;
return time_point_cast<system_clock::duration>
(__from(chrono::seconds(__t)));
}
};
...
}
打印当前时间
std::locale::global(std::locale{ "" });
std::println("UTC: {:L}", system_clock::now());
std::println("UTC: {:L%c}", system_clock::now());
执行时间
auto start { steady_clock::now() }; // time point
auto end { steady_clock::now() }; // time point
auto diff { end - start }; // duration
时间点 time_point
time_point表示的是时间中的某个点,存储为相对于纪元(epoch)的duration,表示开始时间。time_point 总是与特定的 clock 关联,纪元就是所关联 clock 的原点,如UNIX/Linux的纪元为1970-01-01,duration 用秒来度量。
时间点和时间段支持合理的算数运算。
每个time point都关联一个clock。每个clock都知道各自的time point类型。
template<typename _Clock, typename _Dur = typename _Clock::duration>
struct time_point
{
typedef _Clock clock;
typedef _Dur duration;
typedef typename duration::rep rep;
typedef typename duration::period period;
constexpr time_point() : __d(duration::zero())
{ }
constexpr explicit time_point(const duration& __dur)
: __d(__dur)
{ }
// conversions
template<typename _Dur2,
typename = _Require<is_convertible<_Dur2, _Dur>>>
constexpr time_point(const time_point<clock, _Dur2>& __t)
: __d(__t.time_since_epoch())
{ }
// observer
constexpr duration time_since_epoch() const
{ return __d; }
#if __cplusplus > 201703L
constexpr time_point& operator++()
{
++__d;
return *this;
}
constexpr time_point operator++(int)
{ return time_point{__d++}; }
constexpr time_point& operator--()
{
--__d;
return *this;
}
constexpr time_point operator--(int)
{ return time_point{__d--}; }
#endif
// arithmetic
_GLIBCXX17_CONSTEXPR time_point& operator+=(const duration& __dur)
{
__d += __dur;
return *this;
}
_GLIBCXX17_CONSTEXPR time_point& operator-=(const duration& __dur)
{
__d -= __dur;
return *this;
}
// special values
static constexpr time_point min() noexcept
{ return time_point(duration::min()); }
static constexpr time_point max() noexcept
{ return time_point(duration::max()); }
private:
duration __d;
};
time point转换
time_point<steady_clock, milliseconds> tpm { 42'424ms };
time_point<steady_clock, seconds> tps { time_point_cast<seconds>(tpm) };
日期和时区
std::chrono中有不少的日期类
| 类 | 说明 |
|---|---|
| year | 年,范围为 [-32767, 32767] ,有个名为is_leap()的成员函数,闰年为true,否则为false。min(),max() 静态成员函数分别返回最大、最小年份。 |
| month | 月份,[1, 12] 此外还提供了12个用于12个月的命名命名常量,如 std::chrono::January |
| day | 天,范围为[1, 31] |
| … |
可以通过调用std::chrono::get_tzdb()来访问C++标准IANA时区数据库。
const auto& database { get_tzdb() };
for (const auto& timezone : database.zones) {
println("{}", timezone.name());
}
随机数工具
C风格随机数
/* Return a random integer between 0 and RAND_MAX inclusive. */
extern int rand (void) __THROW;
/* Seed the random number generator with the given number. */
extern void srand (unsigned int __seed) __THROW;
随机数引擎
- random_device
该引擎不是基于软件的随机数生成器;它需要连接能真正生成随机数的硬件。随机数生成器的质量由随机数的熵(entropy)考量。如果random device类使用的是基于软件的伪随机数生成器,那么这个类的 entropy() 为0.0 ;如果为硬件设备则返回非0值,为对硬件设备的熵的估计
#include <random>
using namespace std;
int main(int argc, char* argv[]) {
random_device rnd;
println("entropy: {}, min: {}, max: {}, random number: {}", rnd.entropy(), rnd.min(), rnd.max(), rnd());
}
random_device的速度比伪随机数引擎更慢,生成大量随机数用random device生成随机数种子给伪随机数引擎。
- 线形同余引擎
- 梅森旋转
- 带进位减法引擎
随机数引擎适配器
适配器模式的一个实例
随机数
mt19937的预定义的梅森旋转算法。是一个基于软件的生成器,需要随机数种子。
random_device seeder;
const auto seed { seeder.entropy() ? seeder() : time(nullptr) };
mt19937 engine { static_cast<mt19937::result_type>(seed) };
uniform_int_distribution<int> distribution {1, 99}; // [1, 99]
println("{}", distribution(engine));
auto generator { bind(distribution, engine) };
vector<int> values(10);
ranges::generate(values, generator);
println("{}", values); // {:n} 去掉方括号
随机数生成器不是线程安全的
其它词汇类型
下面的几个词汇类型是学习模板元编程和库设计的极佳案例。
在C++中使用union非常的麻烦,需要手动调用vector等类型的析构函数和placement new进行构造。
在学习新的语法是从下面三个方面入手:
- 使用场景
- 实现原理
- 性能
optional
当值可能不存在是可以使用,避免了用特定值表示不存在的情况。
实现方面各个标准库都是用的union+flag的方式。
标准库中optional的实现较为复杂,约1500+行模板代码
template <typename _Up, bool = is_trivially_destructible_v<_Up>>
union _Storage {
...
_Empty_byte _M_empty;
_Up _M_value;
};
// union + flag
template <typename _Tp>
class optional {
......
_Storage<_Stored_type> _M_payload;
bool _M_engaged = false;
};
make_optional(_Tp && __t) noexcept;
std::nullopt;
optional<int> Parse(const string& str) {
try {
return make_optional(stoi(str));
} catch(...) {
return nullopt;
}
}
内存布局与优化:std::optional 的大小通常是 sizeof(T) + 1 字节。但是,标准库实现会利用 [[no_unique_address]] 等属性进行“空基类优化”。如果 T 类型的尾部有填充字节(padding),编译器可能将布尔标志存放在这些填充字节中,使得 sizeof(optional) == sizeof(T),从而实现零额外内存开销。
对于is_trivial的类型开销小;对于string、vector类型有移动操作
std::optional<bool> opt;
// 判断是否有值
if (opt) { /* 有值 */ }
if (opt.has_value()) { /* 同上 */ }
// 取值
opt.value(); // 无值会抛出异常 bad_optional_access
*opt; // 无值是为定义行为
v.value_or(true); // 有值,或使用默认值,有默认构造、析构的开销
C++23引入链式操作: and_then, or_else, transform 这些方法都会返回optional
假设对象的类型为optional<T>
or_else的返回值必须与optional<T>保持一致
template< class F >
constexpr auto transform( F&& f ) &; // 左值版本
template< class F >
constexpr auto transform( F&& f ) &&; // 右值版本(移动)
参数:可调用对象 f,接受 T 类型(当前存储的值)并返回任意类型 U。
返回值:std::optional<U>(其中 U 是 f 的返回类型,非 optional 版本)。若无值则返回std::optional<U>{}
template< class F >
constexpr auto and_then( F&& f ) &;
template< class F >
constexpr auto and_then( F&& f ) &&;
参数:可调用对象 f,接受 T 并返回 std::optional<U>(或可隐式转换为 optional<U> 的类型,但推荐显式返回 optional)。
返回值:若有值返回std::optional<U>,即 f 的返回值类型。若没值则返回std::optional<U>{}。
template< class F >
constexpr optional or_else( F&& f ) &;
template< class F >
constexpr optional or_else( F&& f ) &&;
参数:可调用对象 f,不接受参数(void()),返回 std::optional<T>,其中 T 必须与当前对象持有的类型完全一致(不允许隐式转换)。
返回值:当前 optional(若有值)或 f 的返回值(若空)。
C++23开始optional是一个constexpr类
expected
借鉴了 Rust 语言中 Result 类型的设计,提供了一种比异常更轻量、比错误码更安全清晰的错误处理范式。
expected实现采用union+flag,标准库中大约有1600+代码
template <typename _Tp, typename _Er> class expected {
public:
constexpr expected() noexcept(true) :
_M_val(), _M_has_value(true) {}
union {
remove_cv_t<_Tp> _M_val;
_Er _M_unex;
};
bool _M_has_value;
};
#include <expected>
#include <string>
#include <iostream>
std::expected<int, std::string> divide(int a, int b) {
if (b == 0)
return std::unexpected{"division by zero"};
return a / b;
}
void process() {
auto result = divide(10, 2);
if (result) {
std::cout << "Result: " << *result << '\n';
} else {
std::cout << "Error: " << result.error() << '\n';
}
}
接口:
- has_value(), operator bool: 如果有T,返回true否则false
- value(): 返回类型T的值,如果在包含类型为E的上调用则抛出异常 bad_expected_access
- operator* , ->: 访问类型T的值,如果没有T则为为定义行为
- error(): 如果期望值不包含类型E的值,则为为定义行为
- value_or(): 返回T的值,如果没有T则返回另一个给定值
像optional一样也支持链式调用
variant
union的C++版
variant<int, string, float> v;
v = 12;
v = 12.5f;
v = "An std::string"s;
v.index(); // 当前值的索引号(0开始)
holds_alternative<int>(v); // 是否包含特定类型的值
// 从v中取值,可能抛出bad_variant_access
get<index>(v);
get<T>(v);
// 不抛出异常的版本,失败返回空指针
string* the_string { get_if<string>(&v) };
int* the_int { get_if<int>(&v) };
// visit辅助函数
std::visit([](auto&& value){println("Value = {}", value);}, v);
class MyVisitor {
public:
static void operator()(int i) {}
static void operator()(const string& s) {}
static void operator()(float f) {}
};
std::visit(MyVisitor{}, v);
GCC libstdc++ 的 <variant>
template<typename... _Types>
class variant;
递归 union + _Uninitialized
template <typename _Type, bool> struct _Uninitialized {
template <typename... _Args>
constexpr _Uninitialized(in_place_index_t<0>, _Args &&...__args)
: _M_storage(std::forward<_Args>(__args)...) {}
_Type _M_storage;
};
展开一个 variant<int, double, string> 将生成近似这样的嵌套 union:
union {
_Uninitialized<int> _M_first;
union {
_Uninitialized<double> _M_first;
union {
_Uninitialized<string> _M_first;
// 递归终点为空 union
} _M_rest;
} _M_rest;
}
从C++23开始,variant类是一个constexpr类
any
可包含任意类型值的类。构建any后,可确认其实例中是否包含值,及值的类型。
any_cast()失败会抛出bad_any_cast异常。
any empty; empty.has_value(); // is false
any as_v{3}; as_v.type().name(); // is int
as_v = "An std::string"s; // 可赋值为其它类型
any数据结构
class any {
// Holds either pointer to a heap object or the contained object itself.
union _Storage {
constexpr _Storage() : _M_ptr{nullptr} {}
// Prevent trivial copies of this type, buffer might hold a non-POD.
_Storage(const _Storage &) = delete;
_Storage &operator=(const _Storage &) = delete;
void *_M_ptr;
unsigned char _M_buffer[sizeof(_M_ptr)];
};
enum _Op {
_Op_access,
_Op_get_type_info,
_Op_clone,
_Op_destroy,
_Op_xfer
};
union _Arg {
void *_M_obj;
const std::type_info *_M_typeinfo;
any *_M_any;
};
void (*_M_manager)(_Op, const any *, _Arg *);
_Storage _M_storage;
};
当any赋值时执行下面的操作
/// Construct with a copy of @p __value as the contained object.
template <typename _Tp, typename _VTp = _Decay_if_not_any<_Tp>,
typename _Mgr = _Manager<_VTp> >
any(_Tp &&__value) : _M_manager(&_Mgr::_S_manage) {
_Mgr::_S_create(_M_storage, std::forward<_Tp>(__value));
}
_M_manager函数指针被赋值。当Tp为内部类型时Mgr为_Manager_internal,当非内部类型时为_Manager_external。内部类型的数据用_M_buffer存储,外部数据在堆上存储。
// Manage in-place contained object.
template <typename _Tp> struct _Manager_internal {
static void _S_manage(_Op __which,const any *__anyp,_Arg *__arg){
// The contained object is in _M_storage._M_buffer
auto __ptr = reinterpret_cast<const _Tp *>(&__any->_M_storage._M_buffer);
switch (__which) {
case _Op_access:
__arg->_M_obj = const_cast<_Tp *>(__ptr);
break;
case _Op_get_type_info:
#if __cpp_rtti
__arg->_M_typeinfo = &typeid(_Tp);
#endif
break;
case _Op_clone:
::new (&__arg->_M_any->_M_storage._M_buffer) _Tp(*__ptr);
__arg->_M_any->_M_manager = __any->_M_manager;
break;
case _Op_destroy:
__ptr->~_Tp();
break;
case _Op_xfer:
::new (&__arg->_M_any->_M_storage._M_buffer)
_Tp(std::move(*const_cast<_Tp *>(__ptr)));
__ptr->~_Tp();
__arg->_M_any->_M_manager = __any->_M_manager;
const_cast<any *>(__any)->_M_manager = nullptr;
break;
}
}
template <typename _Up>
static void _S_create(_Storage &__storage, _Up &&__value) {
void *__addr = &__storage._M_buffer;
::new (__addr) _Tp(std::forward<_Up>(__value));
}
template <typename... _Args>
static void _S_create(_Storage &__storage, _Args &&...__args) {
void *__addr = &__storage._M_buffer;
::new (__addr) _Tp(std::forward<_Args>(__args)...);
}
static _Tp *_S_access(const _Storage &__storage) {
// The contained object is in __storage._M_buffer
const void *__addr = &__storage._M_buffer;
return static_cast<_Tp *>(const_cast<void *>(__addr));
}
};
// Manage external contained object.
template <typename _Tp> struct _Manager_external {
static void _S_manage(_Op __which, const any *__anyp, _Arg *__arg){
// The contained object is *_M_storage._M_ptr
auto __ptr = static_cast<const _Tp *>(__any->_M_storage._M_ptr);
switch (__which) {
case _Op_access:
__arg->_M_obj = const_cast<_Tp *>(__ptr);
break;
case _Op_get_type_info:
#if __cpp_rtti
__arg->_M_typeinfo = &typeid(_Tp);
#endif
break;
case _Op_clone:
__arg->_M_any->_M_storage._M_ptr = new _Tp(*__ptr);
__arg->_M_any->_M_manager = __any->_M_manager;
break;
case _Op_destroy:
delete __ptr;
break;
case _Op_xfer:
__arg->_M_any->_M_storage._M_ptr = __any->_M_storage._M_ptr;
__arg->_M_any->_M_manager = __any->_M_manager;
const_cast<any *>(__any)->_M_manager = nullptr;
break;
}
}
template <typename _Up>
static void _S_create(_Storage &__storage, _Up &&__value) {
__storage._M_ptr = new _Tp(std::forward<_Up>(__value));
}
template <typename... _Args>
static void _S_create(_Storage &__storage, _Args &&...__args) {
__storage._M_ptr = new _Tp(std::forward<_Args>(__args)...);
}
static _Tp *_S_access(const _Storage &__storage) {
// The contained object is in *__storage._M_ptr
return static_cast<_Tp *>(__storage._M_ptr);
}
};
};
当通过any_cast取值时最终都会走到:
template <typename _Tp> void *__any_caster(const any *__any) {
// any_cast<T> returns non-null if __any->type() == typeid(T) and
// typeid(T) ignores cv-qualifiers so remove them:
using _Up = remove_cv_t<_Tp>;
// The contained value has a decayed type, so if decay_t<U> is not U,
// then it's not possible to have a contained value of type U:
if constexpr (!is_same_v<decay_t<_Up>, _Up>)
return nullptr;
// Only copy constructible types can be used for contained values:
else if constexpr (!is_copy_constructible_v<_Up>)
return nullptr;
// First try comparing function addresses, which works without RTTI
else if (__any->_M_manager == &any::_Manager<_Up>::_S_manage
#if __cpp_rtti
|| __any->type() == typeid(_Tp)
#endif
) {
return any::_Manager<_Up>::_S_access(__any->_M_storage);
}
return nullptr;
}
类型擦除
本质上还是用void*存储数据,再额外存储类型(或直接用rtti)
元组
tuple实现较为复杂,源码约2600+行,涉及模板元编程的内容,下面仅给出基本实现:
#include <iostream>
// 1. 前置声明
template<typename... Args> struct MyTuple;
// 2. 递归终止:空 tuple
template<> struct MyTuple<> {};
// 3. 核心:递归继承结构(把类型当成脚本一步步解包生成代码)
template<typename Head, typename... Tail>
struct MyTuple<Head, Tail...> : public MyTuple<Tail...> {
Head value; // 存储当前层的第 1 个元素
// 构造函数:把第一个参数留给自己,剩下的传给父类
MyTuple(Head h, Tail... t) : MyTuple<Tail...>(t...), value(h) {}
};
// 4. 辅助工具:利用辅助结构体和特化,根据索引 I 顺着继承链往下找
template<size_t I, typename Tuple> struct MyTupleElement;
// 命中索引 0,当前层的 value 类型就是我们要的
template<typename Head, typename... Tail>
struct MyTupleElement<0, MyTuple<Head, Tail...>> {
using type = Head;
using TupleType = MyTuple<Head, Tail...>;
};
// 索引大于 0,剥离当前层,让编译器继续去父类(Tail...)里找,同时索引减 1
template<size_t I, typename Head, typename... Tail>
struct MyTupleElement<I, MyTuple<Head, Tail...>>
: MyTupleElement<I - 1, MyTuple<Tail...>> {};
// 5. 实现获取元素的 get 函数
template<size_t I, typename... Args>
typename MyTupleElement<I, MyTuple<Args...>>::type&
get(MyTuple<Args...>& t) {
// 强制类型转换为目标父类,然后直接提取 value
using BaseType = typename MyTupleElement<I, MyTuple<Args...>>::TupleType;
return static_cast<BaseType&>(t).value;
}
int main() {
MyTuple<int, double, char> t(42, 3.14, 'k');
std::cout << get<0>(t) << std::endl; // 输出 42
std::cout << get<1>(t) << std::endl; // 输出 3.14
std::cout << get<2>(t) << std::endl; // 输出 k
return 0;
}
总结
本文系统梳理了现代 C++(C++20/23)的核心语言特性与标准库工具,从内存管理、类与对象、继承体系,到模块化编程、模板与泛型、错误处理,再到标准库中的容器、算法、时间日期、随机数以及 optional / expected / variant / any / tuple 等词汇类型。每一部分都结合了关键代码示例和底层实现剖析,旨在帮助读者建立对 C++ 语法语义的深层理解,并在实际工程中做出更合理的技术选型。
无论你是从传统 C++ 向现代 C++ 迁移,还是希望夯实语言基础、深入标准库设计思想,相信本文都能为你提供一份扎实的参考。C++ 的学习永无止境,继续探索,写出更优雅、更高效的代码。
&spm=1001.2101.3001.5002&articleId=151432995&d=1&t=3&u=07795bcb3d464c2a9d144915c7e91bcf)
1882

被折叠的 条评论
为什么被折叠?



