learnlog 日志库开发笔记
目录
技巧
完美转发
- 通用引用
- 通用引用(Universal Reference)是一种
C++11
引入的特性,它是一种模板参数类型,可以接受任何类型的参数,包括值类型、引用类型、常量引用类型、右值引用类型。
语法:
template<typename T>
void foo(T&& t);
- std::forward
std::forward
是C++11
中引入的一个函数模板,它用于将参数转发给另一个函数,同时保留参数的原始类型和值类别,它的参数t
是一个通用引用
类型。如果t
是一个值类型,则std::forward
返回一个右值引用;如果t
是一个引用类型,则std::forward
返回一个左值引用。- 完美转发
- 完美转发(Perfect Forwarding)是一种
C++11
引入的特性,它允许函数模板在调用另一个函数时,完美地转发参数,包括参数的类型、值和引用性。完美转发通过使用std::forward
实现。
例子:
void bar(int& x);
template<typename T>
void foo(T&& t) {
bar(std::forward<T>(t));
}
void bar(int& x) {
x = 10;
}
int main() {
int x = 5;
foo(x); // foo(x) -> bar(x)
cout << x << endl; // 10
return 0;
}
并发编程
条件变量
std::condition_variable
用于实现条件变量的功能。条件变量是一种同步原语,允许线程在某个条件下等待或通知其他线程。
当某个条件不满足时,一个线程可以选择等待,而不是忙等待。
notify()
被调用时,如果没有线程在调用wait()
,那么notify()
不会做任何事情。即notify()
只能唤醒已经在等待状态的线程,而不是将来可能会进入等待状态的线程。
成员函数:
-
wait(lock,
[bool值的lambda函数]
):使当前线程进入等待状态,直到另一个线程调用notify_one()
或notify_all()
。在等待期间,当前线程会释放它持有的所有锁,从而允许其他线程访问共享资源。当收到通知并重新获得锁后,线程会退出等待状态并检查bool 表达式
,如果为true
则继续执行;如果为false
则释放锁并重新进入等待状态; -
wait_for(lock, std::chrono::duration,
[bool值的lambda函数]
):与wait()
类似,但是超时也会退出等待状态。返回值是enum cv_status {timeout, no_timeout}
; -
notify_one():唤醒一个正在等待的线程。如果有多个线程在等待,则只有一个线程会被唤醒;
-
notify_all():唤醒所有正在等待的线程;
例:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool finished = false;
void producer(int id) {
for (int i = 0; i < 5; ++i) {
std::unique_lock<std::mutex> lock(mtx);
data_queue.push(i);
lock.unlock();
std::cout << "Producer " << id << " produced " << i << std::endl;
cv.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
finished = true;
cv.notify_one();
}
void consumer() {
while (!finished) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{return !data_queue.empty() || finished;})
int data = data_queue.front();
data_queue.pop();
std::cout << "Consumer consumed " << data << std::endl;
}
}
int main() {
std::thread producer1(producer, 1);
std::thread producer2(producer, 2);
std::thread consumer1(consumer);
producer1.join();
producer2.join();
consumer1.join();
return 0;
}
- std::unique_lock
std::unique_lock
和std::lock_guard
都是C++标准库中用于实现独占锁的两个类,但它们有一些不同之处。
std::unique_lock:
- 可以手动解锁,也可以析构时自动解锁;
- 可以在锁定和解锁之间进行多次操作;
- 可以在锁定和解锁之间进行条件判断;
- 可以在锁定和解锁之间进行异常处理;
std::lock_guard:
- 只能在构造函数中锁定,在析构函数中自动解锁;
- 只能在锁定和解锁之间进行简单的操作;
- 不能在锁定和解锁之间进行条件判断;
原子变量
参考博客: https://luyuhuang.tech/2022/06/25/cpp-memory-order.html
C++中的atomic
是操作原子变量的模板类。
atomic
操作可通过memory_order
参数传递内存顺序,通过6个memory_order
表达3种模型:
-
sequencial consistent
: 使用memory_order_seq_cst
参数。在该模型下,单一变量的修改顺序一致性扩展到了所有变量,即所有线程看到的所有操作都有一个一致的顺序; -
acquire-release
:
-
使用
memory_order_acquire
、memory_order_release
、memory_order_acq_rel
3个参数; -
对
load
操作使用memory_order_acquire
,对store
操作使用memory_order_release
,可以保证写synchronizes-with
读的关系,在编译器优化时,任何指令都不能重排到acquire
操作的前面, 且不能重排到release
操作的后面; -
consume-release
中对load
操作使用memory_order_release
,相比于acquire-release
,acquire
要求所有的指令都不能重排到它的前面, 而consume
只要求有依赖关系的指令不能重排到它的前面;
relaxed
: 使用memory_order_relaxed
参数,只能保证操作的原子性(即单一变量修改顺序的一致性),不能保证指令重排时的同步关系。
应用场景:
// 利用 acquire-release 模型实现自旋锁
class spinlock {
std::atomic<bool> flag{false};
public:
void lock() {
// exchange 是一种 read-modify-write 操作,它将目标值 (第一个参数) 写入原子变量, 并返回写入前的值
while (flag.exchange(true, std::memory_order_acquire));
}
void unlock() {
flag.store(false, std::memory_order_release);
}
};
// 利用 acquire-release 模型实现线程安全的单例
class App {
static std::atomic<App*> instance;
static std::mutex mu;
public:
App *get_instance() {
auto *p = instance.load(std::memory_order_acquire); // (1)
if (!p) {
std::lock_guard<std::mutex> guard(mu);
if (!(p = instance.load(std::memory_order_relaxed))) { // (2)
p = new App;
instance.store(p, std::memory_order_release); // (3)
}
}
return p;
}
};
- std::atomic_flag
std::atomic_flag
是C++11
引入的原子类型,用于表示一个原子标志位,这个标志位只有设置(true)
和未设置(false)
两种状态。可以用它实现低开销的锁、信号量等。
- test_and_set(): 如果标志位已设置则返回
true
,否则返回false
并设置标志位; - clear(): 清除标志位;
- test(): 检查标志位;
std::promise 和 std::future
std::promise
和 std::future
是 C++11
中引入的两个类模板,用于实现异步编程。它们是配套的类,用于在异步操作中传递结果。
-
std::promise
用于设置异步操作的结果,提供如下成员函数:- set_value(): 设置结果;
- set_exception(): 设置异常;
- set_write_callback(): 设置等待回调函数;
- get_future(): 获取关联的
std::future
对象;
-
std::future
用于获取异步操作的结果,提供如下成员函数:- get(): 阻塞至获取到结果;
- wait(): 等待操作完成;
- wait_for(): 等待操作完成,指定超时时长;
- wait_until(): 等待操作完成,指定超时时间点;
std::promise
只能设置一次结果,std::future
只能获取一次结果,否则会抛出std::future_error
异常;
std::promise
对象可以与std::future
对象一对多绑定,但std::future
对象只能与std::promise
对象一对一绑定;
如果在future.get()
的阻塞等待时promise
在未设置值或异常的情况下被销毁,那么future
会抛出std::future_error
,错误码是std::future_errc::broken_promise
。
CAS
CAS
操作,即比较并交换(Compare-And-Swap)
,是一种重要的原子操作,用于实现无锁编程或多线程同步。
在 c++
的 std::atomic
模板类中,有如下实现 CAS
操作的函数:
bool atomic<>.compare_exchange_weak(T& expected, T desired, // 操作成功时 expected 的值不变,失败时 expected 会被改为原子变量的当前值
std::memory_order success, // 成功时的内存序
std::memory_order failure); // 失败时的内存序,一般低于成功时
bool atomic<>.compare_exchange_strong(T& expected, T desired,
std::memory_order success,
std::memory_order failure);
compare_exchange_strong
不会出现虚假失败(spuriously fail)
,但也因此减少了编译器和硬件优化的空间。通常情况下,compare_exchange_strong
只调用一次,而 compare_exchange_weak
会循环调用至交换成功。
- spuriously fail
- 即比较条件实际满足,但是
CAS
操作失败的情况,这种情况的出现通常与硬件优化
、弱内存模型
、缓存一致性
等因素相关。compare_exchange_wrak
给编译器和处理器更多的自由度来优化代码,但是也会出现spuriously fail
的问题。#include <atomic> std::atomic<int> atomic_value(1); int main() { int expected = 1; int desired = 2; // 如果操作失败并且expected没有被改变,则继续尝试 while (!atomic_value.compare_exchange_weak(expected, desired) && expected == 1) {} // 或者 atomic_value.compare_exchange_strong(expected, desired); // 此时,要么atomic_value已经成功更新为desired,要么它不再是1 return 0; }
C++11
- final
- 在
C++11
中,final
是一个关键字,用于修饰虚函数和类。在虚函数后面加final
时表示该函数不能被子类重写;在类后面加final
时,表示该类不能被继承。 - 默认的复制构造函数和赋值运算符
- 默认的复制构造函数通过
MyClass(const MyClass &) = default
指定,默认的赋值运算符通过MyClass& operator=(const MyClass &) = default
指定。
它们复制的规则如下:
- 如果类中有成员变量,则会复制这些成员变量的值;
- 如果类中有基类,则会调用基类的复制构造函数 / 赋值运算符;
- 如果类中有虚函数,则不会复制虚函数表;
所以如果类中有指针成员变量,它们会导致指针的浅拷贝;如果类中有基类,它们可能不会正确地复制或赋值基类的成员变量。
- 虚函数表
- 每个类都有一个虚函数表,它是一个存储了类中所有虚函数的指针的数组。
虚函数表中的每个指针都指向一个具体的函数实现;这个函数实现可能是类本身定义的,也可能是从基类继承来的;
每个类的实例都有一个指向该类虚函数表的指针,这个指针通常被称为vptr(Virtual Table Pointer)
。 - constexpr
constexpr
是C++11
中引入的一个关键字,用于声明常量表达式。它告诉编译器,这个表达式在编译时就可以求值,结果是一个常量。- 常量表达式
- 常量表达式通常用于
数组大小
、枚举值
、模板参数
、switch语句
、常量变量
、类型转换
、sizeof()
等。 - 初始化
- 在
C++
中,{}
和()
是两种不同的初始化方式。
()
是函数调用运算符,用于调用函数并传递参数。当你使用()
初始化一个对象时,编译器会将其解释为函数调用,尝试找到一个与参数列表匹配的构造函数。
{}
是聚合初始化运算符,用于初始化聚合类型(如结构体、类、数组等)。当你使用 {}
初始化一个对象时,编译器会将其解释为聚合初始化,直接初始化对象的成员变量。
当使用()
初始化时,编译器可能会将其解释为函数调用,而不是构造函数调用;另外,当使用()
初始化时,编译器可能会进行隐式转换,以匹配构造函数的参数列表。所以使用{}
可以避免构造函数的歧义和隐式转换问题。
- 类型特性
- 类型特性(type trait)是
C++
中用于检查和操作类型属性的工具,属于模板元编程的一部分,用于在编译时检查和操作类型的属性。
类型特性通常是一些模板类或函数,它们通过模板参数推导出类型的属性。在C++
标准库中有很多内置的类型特性,如 std::is_integral
、 std::is_pointer
、 std::is_template
、 std::is_convertible
、std::is_same
等。
static_assert(std::is_convertible<int, double>::value, ""); // true
static_assert(std::is_convertible<double, int>::value, ""); // false
- volatile
volatile
是一个关键字,在C/C++
中用来修饰变量,告诉编译器这个变量的值可能会被其他线程或外部设备修改,因此编译器不能对其进行优化。
当一个变量被声明为volatile
时,编译器会做出以下保证:
- 每次访问该变量时,编译器都会从内存中读取最新的值,而不是使用寄存器中的缓存值;
- 编译器不会对该变量进行优化;
- 编译器会确保该变量的访问是原子的,即使在多线程环境中,也不会出现访问该变量时的数据竞争;
- unique_ptr
unique_ptr
是C++11
中引入的一种智能指针,它具有独占所有权、自动管理内存 无法复制、可以移动、可以释放所有权、可以重置、异常安全等特点。
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor called" << std::endl; }
~MyClass() { std::cout << "MyClass destructor called" << std::endl; }
};
int main() {
// 使用 unique_ptr 动态分配一个 MyClass 对象
std::unique_ptr<MyClass> ptr(new MyClass());
// 尝试复制 unique_ptr,会导致编译错误
// std::unique_ptr<MyClass> ptr2 = ptr;
// 移动 unique_ptr
std::unique_ptr<MyClass> ptr2 = std::move(ptr);
// 原来的 unique_ptr 失效
// ptr->~MyClass(); // 错误,ptr 已经失效
// 释放所有权
MyClass* rawPtr = ptr2.release();
// 重置指向的对象
ptr2.reset(new MyClass());
return 0;
}
- static_assert
static_assert
是C++11
中引入的一种新特性,允许在编译时检查某些条件,并在条件不满足时生成编译时错误。
语法: static_assert(const_expr, string_literal)
,其中 const_expr
是用于检查的常量表达式(即编译时就能确定值的表达式),string_literal
是错误信息描述。
- std::strstr()
- 用于在一个字符串中查找另一个字符串。如果找到返回
模板串
在目标串
的首地址,否则返回nullptr
。内部用KMP
算法实现。
语法: const char* std::strstr(const char* 目标串, const char* 模板串);
- std::all_of() / any_of() / none_of()
- 用于检查迭代器范围内的元素是否满足某个条件,返回值为
bool
。
语法: std::all/any/none_of(Iter begin, Iter end, lambda expr)
。
- std::hash
- 用于计算一个对象的哈希值。常见使用方式如下:
std::hash<T> hasher; size_t hash_val = hasher(obj);
;size_t hash_val = std::hasher<T>{}(obj)
;
这是一个模板类,返回类型为 size_t
。基本数据类型以及 STL
容器都有默认的哈希值计算方法,自定义类型需要自行实现:
// std::hash<float>
template<>
struct hash<float> {
size_t operator()(float x) const {
return x * 0.61803398875f;
}
};
- std::enable_shared_from_this
C++11
中的一个模板类,使得一个对象可以安全的从内部获取自身的shared_ptr
。
语法:
class A : public std::enable_shared_from_this<A> {
public:
void foo() {
auto this_ptr = shared_from_this();
}
};
std::enable_shared_from_this
只能用于类的继承,不可以直接使用。同时,shared_from_this()
只能在类的成员函数中调用。
- std::array
C++11
引入的数组对象,是一个固定大小的数组类,大小在编译时确定。它具有如下特点:
- 元素存储在栈上;
- 不支持动态大小调整;
- 访问元素时不需要动态内存分配或释放;
- 传递参数时可以传递整个对象;
设计模式
- Factory
Factory
提供了一种创建对象的方式,而无需指定创建对象的具体类。它的主要目的是:
- 将对象的创建和使用分离,使得创建对象的过程更加灵活和可扩展;
- 封装对象的创建过程,使得创建对象的细节对用户透明;
- Singleton
Singleton
一般定义一个类,限制其只能实例化一次,并提供一个全局访问点来访问该实例。
它的设计结构包括:
Singleton
类,将构造函数定义为私有的,不允许从外部实例化;- 静态实例,定义
Singleton
类的静态实例,用于存储唯一的实例对象; - 静态的获取实例方法,用于获取实例对象;
符号
在 C++
中,符号 (symbol)
主要指的是编译器和链接器使用的一些标识符。符号可以是函数、变量、类型、命名空间等。在编译阶段,编译器会生成符号表,记录下所有定义和引用的符号;在链接阶段链接器会解析所有符号,确保所有引用的符号都有相应的定义。
-
强符号
- 由定义明确的全局变量和函数生成;
- 在链接过程中具有唯一性,即在整个程序中,一个强符号只能定义一次;
- 如果同一个强符号被定义多次,链接器会报重复定义错误;
-
弱符号
- 通常由未初始化的全局变量或用特定关键字(如
__attribute__((weak))
)声明的变量和函数生成; - 在链接过程中不具有唯一性,多个弱符号可以共存;
- 如果强符号和弱符号同名,强符号优先。如果同一个弱符号被定义多次,链接器会选择其中一个定义(通常是第一个);
- 通常由未初始化的全局变量或用特定关键字(如
内联函数
(inline)
通常被定义为弱符号,以便多个目标文件中可以定义同名的内联函数。
内存
栈与堆
- 堆区的内存通常是从低地址向高地址增长的。内存分配是动态的,由程序员使用
malloc
、new
等函数进行分配和释放; - 栈区的内存通常是从高地址向低地址增长的。栈内存用于函数调用时的临时变量,分配和释放是自动的,由编译器管理。
new 与 malloc
new
和 malloc
的区别如下:
- 构造函数:
new
分配指定类型的内存,并调用该类型的构造函数来初始化对象;malloc
分配指定大小的内存,以字节为单位,不调用构造函数; - 释放内存:
new
使用delete
释放内存,并调用对象的析构函数;malloc
使用free
释放内存,不调用析构函数; - 类型安全:
new
返回指定类型的指针,类型安全;malloc
返回void*
,需要显式转换为目标类型,不类型安全; - 异常处理:
new
在内存分配失败时抛出std::bad_alloc
异常,而malloc
在内存分配失败时返回NULL
;
内存栅栏
内存栅栏
是一种同步机制,它确保内存操作的执行顺序,并防止编译器和处理器对内存访问进行重排序。在 c++11
中,内存栅栏
通过 std::atomic_thread_fence
实现。
原型: void atomic_thread_fence(memory_order order);
其中 memory_order
包括:
- memory_order_relaxed
- memory_order_acquire
- memory_order_release
- memory_order_acq_rel
- memory_order_seq_cst
- memory_order_acq_rel
- memory_order_consume (不推荐)
acquire
栅栏确保了:
1. 栅栏之后的读操作不会被重排到栅栏之前;
2. 栅栏之前由其他线程执行并加上 release
栅栏的写操作是可见的;
release
栅栏确保了:
1. 栅栏之前的写操作不会被重排到栅栏之后;
2. 栅栏之前的写操作对于在加上 acquire
栅栏之后的读操作是可见的;
acquire栅栏
和release栅栏
是多线程下的一组操作,分析时不应该脱离多线程的环境,也不应该单独讨论。 可以理解为release
之后,acquire
之前没有操作。
- placement new
placement new
是一种特殊的 new 操作符,允许在预先分配的内存上构造对象。在性能关键的代码中,通过预先分配和对齐内存可以减少内存分配的开销,但是使用placement new
后,需要手动调用析构函数并释放内存。
// 分配一块足够大的内存
void* buffer = operator new(sizeof(MyClass));
// 在这块内存上使用 placement new 构造对象
MyClass* obj = new (buffer) MyClass();
- std::alignment_of
std::alignment_of<T>
是c++11
引入的一个类型特性(type trait)
,它用于确定某个类型的对齐要求,即该类型必须分配在的内存边界。
// 这个函数把 ptr 向高位移动到最近的 U 类型的内存边界上(刚好在边界上则不移动)
template<typename U>
static inline char* align_for(char* ptr)
{
const std::size_t alignment = std::alignment_of<U>::value;
return ptr + (alignment - (reinterpret_cast<std::uintptr_t>(ptr) % alignment)) % alignment;
}
- std::max_align_t
std::max_align_t
是c++11
引入的一个类型别名,代表了拥有最大对齐要求的基本数据类型,这个类型通常是long double
,对齐要求是16
字节。- alignas 和 alignof
alignas
和alignof
是c++11
引入的两个关键字。
-
alignas(typename / const_expr) object
用于指定变量或类的对齐方式:struct alignas(64) MyStruct { /* ... */ }; // 指定结构体的对齐方式为 64 字节 alignas(MyStruct) int i; // 指定变量 i 的对齐方式为 MyStruct 的对齐方式
-
alignof(object)
用于查询变量或类的对齐方式:alignof(MyStruct) alignof(std::max_align_t)
- false sharing
false sharing
是一种在多核处理器系统中出现的性能优化问题,它发生在多个线程在不同的核心上修改不同的变量时,这些变量恰好位于同一缓存行(一般是64字节)上,从而造成不必要的缓存刷新问题。
避免 false sharing
的方法:
- 缓存行对齐: 通过填充,使得每个变量占据整个缓存行的大小;
- 数据局部性: 尽量保持线程本地数据在内存中的局部性,减少不同线程间数据的交叉;
- 使用专门的库;
分支预测
- __builtin_expect
__builtin_expect
是GCC
提供的一个内置函数,用于分支预测优化。分支预测是一个重要的特性,它可以帮助处理器在等待分支结果时继续执行指令流水线,从而提高执行效率。
常见用法:
#if defined(__GNUC__)
#define LIKELY(x) __builtin_expect(!!(x), 1)
#define UNLIKELY(x) __builtin_expect(!!(x), 0)
#endif
#if defined(__GNUC__)
static inline bool (likely)(bool x) { return __builtin_expect((x), true); }
static inline bool (unlikely)(bool x) { return __builtin_expect((x), false); }
#else
static inline bool (likely)(bool x) { return x; }
static inline bool (unlikely)(bool x) { return x; }
#endif
静态变量
在 C++
中,静态变量 (static)
的生命周期一般是整个程序运行期间,作用域因使用位置不同而变化:
位置 | 作用域 | 特点 |
---|---|---|
局部静态变量 | 函数/代码块内 | 首次调用时初始化 |
类的静态成员变量 | 类作用域 | 类内声明,类外定义,所有实例共享 |
类的静态成员函数 | 类作用域 | 无 this 指针,不能访问类的非静态成员 |
全局静态变量或函数 | 声明文件内 | 在头文件中可能引发多文件冲突 |
需要注意:
- 若在头文件中声明了全局静态变量,那么每个包含该头文件的源文件都会有该变量的副本,这些副本各自独立,可能会引发不符合预期的行为;
- 类的静态成员变量必须要在类内声明,类外定义,否则会报链接错误;
C++11
后,局部静态变量的初始化是线程安全的;
如果在多个源文件中定义了同名的强符号,会引发 重复定义
的冲突,有如下解决办法:
- 通过
static
关键字,将符号
从外部链接
改为内部链接
; - 将
符号
封装在匿名命名空间中,使其仅在当前文件可见(内部链接)
; - 使用
inline
,定义弱符号
;
对于全局变量:
- 通过
局部变量
、单例
、依赖注入
等方式减少全局变量的使用; - 在头文件中声明变量为
extern
,在一个源文件中定义,其它文件通过头文件共享; C++17
后,可以在头文件定义inline
变量,允许多次定义但指向同一实体。