文章目录
- [第二章 C++20标准库特性](#第二章 C++20标准库特性)
-
- [2.5 原子智能指针](#2.5 原子智能指针)
-
- 2.5.1原子智能指针语法格式和常用方法
- [2.5.2 使用总结](#2.5.2 使用总结)
- [2.6 std::source_location](#2.6 std::source_location)
-
- [2.6.1 source_location 语法格式](#2.6.1 source_location 语法格式)
- [2.6.2 举例](#2.6.2 举例)
- [2.7 位操作函数](#2.7 位操作函数)
本文记录C++20新特性之原子智能指针、std::source_location和位操作函数。
第二章 C++20标准库特性
2.5 原子智能指针
C++11引入了智能指针,shared_ptr和unique_ptr是内存管理的利器。但是,shared_ptr不是线程安全的,虽然,std::shared_ptr 的引用计数控制块是线程安全的,但修改它指向的对象并不是线程安全的。所以,在多线程中读写同一个全局的 std::shared_ptr对象,面临数据竞争风险。
C++20之前的解决方法是,在多线程环境下,对共享变量加锁,以确保线程安全。
C++20 的解决方案: 直接特化 std::atomic 模板,允许 std::atomic<std::shared_ptr>。这使得智能指针的原子操作变得类型安全且自然。
2.5.1原子智能指针语法格式和常用方法
语法格式:原子智能指针使用起来像使用std::atomic 一样,只是模板参数是智能指针。
使用方式如下:
cpp
struct Data
{
int value;
};
void test()
{
// 定义一个原子的 shared_ptr
std::atomic<std::shared_ptr<Data>> atomic_ptr;
// 定义一个原子的 weak_ptr
std::atomic<std::weak_ptr<Data>> atomic_weak;
}
常用方法:
- std::atomic<std::shared_ptr> 提供了标准原子类型的一系列方法,确保了对指针本身操作的原子性。
- store(val): 原子地替换存储的指针。
- load(): 原子地返回当前指针的一个副本(增加引用计数)。
- exchange(val): 原子地替换指针并返回旧值。
- compare_exchange_strong/weak(...): CAS(Compare-And-Swap)操作,用于无锁编程。
- wait/notify: 支持 C++20 的原子等待机制(类似于条件变量,但更轻量)。
cpp
void test()
{
std::atomic<std::shared_ptr<int>> ptr = make_shared<int>(33);
// 1 原子写入
auto new_ptr = std::make_shared<int>(42);
ptr.store(new_ptr, std::memory_order_release); // 都指向了ptr
cout << "引用计数 " << new_ptr.use_count() << endl;
// 引用计数 2
// 输出 ptr 的值
cout << "ptr = " << *ptr.load() << endl;
// 42
cout << " new_ptr = " << *new_ptr << endl;
// 42
cout << "ptr 地址 = " << ptr.load().get() << endl;
// ptr 地址 = 0000018FDC3387A0
cout << "new_ptr 地址 = " << new_ptr.get() << endl;
// new_ptr 地址 = 0000018FDC3387A0
cout << "引用计数 " << ptr.load().use_count() << endl;
// 引用计数 3
// 2 原子读取,load 返回副本
auto loaded_ptr = ptr.load(std::memory_order_acquire);
std::cout << "Value: " << *loaded_ptr << std::endl;
// Value: 42
}
2.5.2 使用总结
在多线程环境下,可以使用 原子智能指针来确保线程安全,这虽然比裸指针的原子操作慢,但比使用mutex 保护 shared_ptr快,也可以避免死锁风险。
2.6 std::source_location
在C++20之前,为了记录当前代码所在文件名,行号,函数名,使用如下方式:
cpp
// 必须使用宏,否则 __LINE__ 会是 log 函数内部的行号,而不是调用处的行号
#define LOG(msg) log_message(msg, __FILE__, __LINE__, __func__)
void log_message(const char* msg, const char* file, int line, const char* func) {
// ...
}
这种方式缺点:
1 宏是文本替换:容易导致命名冲突,且调试困难。
2 参数传递困难:你无法轻松地将"调用者的位置"作为一个默认参数传递给函数,因为 LINE 在参数默认值处展开时,行为可能不符合预期(或者需要非常复杂的宏技巧)。
3 类型不安全:FILE 只是个 const char*,没有封装。
C++20 引入了 std::source_location,提供了一种标准、类型安全且零开销的方式来获取源代码信息。
2.6.1 source_location 语法格式
std::source_location 是一个类,包含在 <source_location>头文件中,编译器会对其进行特殊处理。当它作为函数的默认参数时,编译器会自动填入调用该函数时的源代码位置。
source_location 提供了一个静态函数 current(),我们可以使用这个函数获取代码的文件名,行号等信息。
常用方法:
- static current(): 获取当前代码位置的 source_location 对象。
- file_name(): 返回文件名 (const char*)。
- function_name(): 返回函数名 (const char*)。
- line(): 返回行号 (std::uint_least32_t)。
- column(): 返回列号 (std::uint_least32_t)。
使用示例如下:
cpp
void log(const char* message, const std::source_location& location = std::source_location::current())
{
std::cout << "File: " << location.file_name() << "\n"
<< "Line: " << location.line() << "\n"
<< "Func: " << location.function_name() << "\n"
<< "Message: " << message << "\n";
}
void test()
{
log("This is a log message.");
/*
File: E:\cpp_learning\newFeature\newFeature\newFeature.cpp
Line: 2579
Func: void __cdecl sp60::test(void)
Message: This is a log message.
*/
}
2.6.2 举例
使用C++20的source_location实现一个不依赖宏的日志函数。
cpp
enum class LogLevel
{
INFO,
ERROR
};
class Logger
{
public:
// 注意:location 必须是默认参数
static void log(LogLevel level, std::string_view message,
const std::source_location& loc = std::source_location::current())
{
std::cout << std::format("[{}] {}:{}:{} | {}\n",
(level == LogLevel::INFO ? "INFO" : "ERROR"),
loc.file_name(),
loc.line(),
loc.function_name(),
message);
}
};
void complex_calculation()
{
Logger::log(LogLevel::INFO, "Calculation started");
//[INFO] newFeature.cpp:2592: complex_calculation(void) | Calculation started
Logger::log(LogLevel::ERROR, "Calculation failed");
// [ERROR] newFeature.cpp:2594: complex_calculation(void) | Calculation failed
}
void test()
{
complex_calculation();
}
2.7 位操作函数
C++20 引入了 头文件,提供了一组标准、跨平台、高性能且类型安全的位操作函数,给位操作提供了便利。
下面介绍bit库中常用的方法。
计数类 (Counting)
- std::popcount(x): 计算二进制中 1 的个数 (Population Count)。
- std::countl_zero(x): 从左(最高位)开始计算连续 0 的个数 (Count Leading Zeros)。
- std::countl_one(x): 从左开始计算连续 1 的个数。
- std::countr_zero(x): 从右(最低位)开始计算连续 0 的个数 (Count Trailing Zeros)。
- std::countr_one(x): 从右开始计算连续 1 的个数。
cpp
int test()
{
uint8_t x = 0b00101100; // 44
std::cout << std::format("Num: {:08b}\n", x);
std::cout << "计算1的数量为:: " << std::popcount(x) << "\n";
// 3
std::cout << "从最高位计算,连续0的个数: " << std::countl_zero(x) << "\n";
// 2
std::cout << "从右开始计算连续1的个数: " << std::countr_zero(x) << "\n";
// 2
return 0;
}
幂运算与对数类 (Powers & Logarithms)
- std::has_single_bit(x): 检查 x 是否是 2 的幂(即二进制中是否只有一个 1)。
- std::bit_ceil(x): 返回不小于 x 的最小的 2 的幂(向上取整到 2 的幂)。
- std::bit_floor(x): 返回不大于 x 的最大的 2 的幂(向下取整到 2 的幂)。
- std::bit_width(x): 返回表示 x 所需的最小二进制位数。
cpp
void test()
{
uint32_t val = 100;
bool is_power_2 = std::has_single_bit(val); // 2的幂
// false
auto next_pow2 = std::bit_ceil(val); // 返回不小于x 最小的2的幂
// 128
auto prev_pow2 = std::bit_floor(val); // 返回不大于x 最大的2的幂
// 64
auto width = std::bit_width(val); // 表示x的二进制表示所需的位数
// 7 (因为 100 是 1100100)
}
字节序转换 (Endian)
std::endian: 一个枚举类,用于在编译期检测系统字节序。
cpp
void test()
{
if constexpr (std::endian::native == std::endian::little)
{
std::cout << "Little Endian System\n";
}
else {
std::cout << "Big Endian System\n";
}
// Little Endian System
}
类型转换 (Bit Cast) : 使用std::bit_cast(From),将From类型的二进制位原封不动解释为To类型。
cpp
float f = 3.14f;
// 安全地将 float 的位模式转换为 uint32_t
uint32_t i = std::bit_cast<uint32_t>(f);