learnlog 日志库开发笔记

目录

技巧

完美转发

通用引用
通用引用(Universal Reference)是一种C++11引入的特性,它是一种模板参数类型,可以接受任何类型的参数,包括值类型、引用类型、常量引用类型、右值引用类型。

语法:

template<typename T>
void foo(T&& t);

std::forward
std::forwardC++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()只能唤醒已经在等待状态的线程,而不是将来可能会进入等待状态的线程。

成员函数:

  1. wait(lock, [bool值的lambda函数]):使当前线程进入等待状态,直到另一个线程调用notify_one()notify_all()。在等待期间,当前线程会释放它持有的所有锁,从而允许其他线程访问共享资源。当收到通知并重新获得锁后,线程会退出等待状态并检查 bool 表达式,如果为 true 则继续执行;如果为 false释放锁重新进入等待状态

  2. wait_for(lock, std::chrono::duration, [bool值的lambda函数]):与wait()类似,但是超时也会退出等待状态。返回值是enum cv_status {timeout, no_timeout}

  3. notify_one():唤醒一个正在等待的线程。如果有多个线程在等待,则只有一个线程会被唤醒;

  4. 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_lockstd::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种模型:

  1. 使用memory_order_acquirememory_order_releasememory_order_acq_rel 3个参数;

  2. load操作使用memory_order_acquire,对store操作使用memory_order_release,可以保证写 synchronizes-with 读的关系,在编译器优化时,任何指令都不能重排到acquire操作的前面, 且不能重排到release操作的后面;

  3. consume-release中对load操作使用memory_order_release,相比于acquire-releaseacquire要求所有的指令都不能重排到它的前面, 而consume只要求有依赖关系的指令不能重排到它的前面;

应用场景:

// 利用 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_flagC++11 引入的原子类型,用于表示一个原子标志位,这个标志位只有 设置(true)未设置(false) 两种状态。可以用它实现低开销的锁、信号量等。

std::promise 和 std::future

std::promisestd::futureC++11 中引入的两个类模板,用于实现异步编程。它们是配套的类,用于在异步操作中传递结果。

  1. std::promise 用于设置异步操作的结果,提供如下成员函数:

    • set_value(): 设置结果;
    • set_exception(): 设置异常;
    • set_write_callback(): 设置等待回调函数;
    • get_future(): 获取关联的 std::future 对象;
  2. 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
constexprC++11中引入的一个关键字,用于声明常量表达式。它告诉编译器,这个表达式在编译时就可以求值,结果是一个常量。
常量表达式
常量表达式通常用于数组大小枚举值模板参数switch语句常量变量类型转换sizeof()等。
初始化
C++中,{}()是两种不同的初始化方式。

()函数调用运算符,用于调用函数并传递参数。当你使用()初始化一个对象时,编译器会将其解释为函数调用,尝试找到一个与参数列表匹配的构造函数。

{}聚合初始化运算符,用于初始化聚合类型(如结构体、类、数组等)。当你使用 {}初始化一个对象时,编译器会将其解释为聚合初始化,直接初始化对象的成员变量。

当使用()初始化时,编译器可能会将其解释为函数调用,而不是构造函数调用;另外,当使用()初始化时,编译器可能会进行隐式转换,以匹配构造函数的参数列表。所以使用{}可以避免构造函数的歧义和隐式转换问题。

类型特性
类型特性(type trait)是C++中用于检查和操作类型属性的工具,属于模板元编程的一部分,用于在编译时检查和操作类型的属性。

类型特性通常是一些模板类或函数,它们通过模板参数推导出类型的属性。在C++标准库中有很多内置的类型特性,如 std::is_integralstd::is_pointerstd::is_templatestd::is_convertiblestd::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时,编译器会做出以下保证:

  1. 每次访问该变量时,编译器都会从内存中读取最新的值,而不是使用寄存器中的缓存值;
  2. 编译器不会对该变量进行优化;
  3. 编译器会确保该变量的访问是原子的,即使在多线程环境中,也不会出现访问该变量时的数据竞争;
unique_ptr
unique_ptrC++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_assertC++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
用于计算一个对象的哈希值。常见使用方式如下:

这是一个模板类,返回类型为 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 一般定义一个类,限制其只能实例化一次,并提供一个全局访问点来访问该实例。

它的设计结构包括:

符号

C++ 中,符号 (symbol) 主要指的是编译器和链接器使用的一些标识符。符号可以是函数、变量、类型、命名空间等。在编译阶段,编译器会生成符号表,记录下所有定义和引用的符号;在链接阶段链接器会解析所有符号,确保所有引用的符号都有相应的定义。

内联函数(inline)通常被定义为弱符号,以便多个目标文件中可以定义同名的内联函数。

内存

栈与堆

new 与 malloc

newmalloc 的区别如下:

内存栅栏

内存栅栏 是一种同步机制,它确保内存操作的执行顺序,并防止编译器和处理器对内存访问进行重排序。在 c++11 中,内存栅栏 通过 std::atomic_thread_fence 实现。

原型: void atomic_thread_fence(memory_order order);

其中 memory_order 包括:

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_tc++11 引入的一个类型别名,代表了拥有最大对齐要求的基本数据类型,这个类型通常是 long double,对齐要求是 16 字节。
alignas 和 alignof
alignasalignofc++11 引入的两个关键字。
false sharing
false sharing 是一种在多核处理器系统中出现的性能优化问题,它发生在多个线程在不同的核心上修改不同的变量时,这些变量恰好位于同一缓存行(一般是64字节)上,从而造成不必要的缓存刷新问题。

避免 false sharing 的方法:

分支预测

__builtin_expect
__builtin_expectGCC 提供的一个内置函数,用于分支预测优化。分支预测是一个重要的特性,它可以帮助处理器在等待分支结果时继续执行指令流水线,从而提高执行效率。

常见用法:

#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 指针,不能访问类的非静态成员
全局静态变量或函数 声明文件内 在头文件中可能引发多文件冲突

需要注意:

如果在多个源文件中定义了同名的强符号,会引发 重复定义 的冲突,有如下解决办法:

  1. 通过 static 关键字,将 符号外部链接 改为 内部链接
  2. 符号 封装在匿名命名空间中,使其仅在当前文件可见 (内部链接)
  3. 使用 inline,定义 弱符号

对于全局变量:

  1. 通过 局部变量单例依赖注入等方式减少全局变量的使用;
  2. 在头文件中声明变量为 extern,在一个源文件中定义,其它文件通过头文件共享;
  3. C++17 后,可以在头文件定义 inline 变量,允许多次定义但指向同一实体。