使用std::list的splice方法实现LRU Cache

splice函数通过重新排列链表指针,将一个std::list中的节点转移到另一个std::list中。在元素的转移过程中不会触发元素的拷贝或者移动。因此,调用splice函数之后,元素现有的引用和迭代器都不会失效。

下面是一个将listA中所有节点附加到listB的一个简单代码示例,转移的过程不会导致listA中元素的引用和迭代器失效:

// Note: c++17 required below. (For CTAD(Class template argument deducation))
std::list listA{1, 2, 3};
std::list listB{4, 5, 6};

auto it = listA.begin();   // Iterator to 1

// Append listA to listB
listB.splice(listB.end(), listA);

// All listA elements transferred to listB
std::cout << listB.size() << " " << listA.size() << std::endl;   // 6 0

// Prints Below: 4 5 6 1 2 3
for (auto i : listB) {
    std::cout << i << " ";
}
std::cout << std::endl;

// Iterator still valid
std::cout << *it << std::endl;   // 1

当然,我们也可以在不使用splice的情况下将一个 list 中的元素转移到另一个 list 中,但是需要将原 list 中的元素删除,并在目标 list 中插入新的元素。删除和新增元素对于较小的对象(例如 int)是可以接受的,但是对于较大的对象来说,由于需要调用拷贝/移动构造和析构函数,所以成本会很高。

深入理解 enable_shared_from_this

shared_ptr是一种共享所有权的智能指针,它允许我们安全地访问和管理对象的生命周期。shared_ptr的多个实例通过共享控制块结构来控制对象的生命周期。 控制块维护了引用计数(reference count),弱引用计数(weak count)和其他必要的信息,通过这些信息,控制块能够确定一个对象在内存中是否可以被安全销毁。

当使用原始指针构造或者初始化一个shared_ptr时,将会创建一个新的控制块。为了确保一个对象仅由一个共享的控制块管理,必须通过复制已存在的shared_ptr对象来创建一个新的shared_ptr实例,例如:

void good()
{
  auto p{new int(10)}; // p is int*
  // create additional shared_ptr from an existing shared_ptr
  std::shared_ptr<int> sp1{p};
  // sp2 shares control block with sp1
  auto sp2{sp1};
}

而使用指向已由shared_ptr管理的对象的原始指针来初始化另一个shared_ptr时,会创建一个新的控制块来管理该对象, 这样同一个对象就同时被多个控制块管理,这会导致 undefined behavior,例如:

c++ 中 unique_ptr 的一些使用技巧

c++11 对智能指针做了很大的优化,废弃了 c++98 中的auto_ptr,引入了三种新的智能指针:unique_ptrshared_ptrweak_ptr。 本文将针对unique_ptr的一些使用技巧做一些整理和归纳。在正式开始之前,我们首先来回顾一下unique_ptr的特点:一个unique_ptr对象内包含一个原始指针,该unique_ptr对象负责管理原始指针的生命周期。 一个unique_ptr对象始终是其关联的原始指针的唯一拥有者。

在了解了unique_ptr的特点之后,我们来具体看看日常开发中unique_ptr的一些使用场景和技巧。

在开发中,我们经常会遇到或者写出类似于下面这样的逻辑:

void somefunc() {
    Object obj = new Object;
    // ...
    if (/* event 1 */) {
        delete obj;
        return;
    }

    if (/* event 2 */) {
        delete obj;
        return;
    }
    delete obj;
}

对于这样的代码,写起来很麻烦,看上去也及其丑陋。以前我们常用的一种优化手段就是使用goto,而在 c++11 之后,我们有了一种更加优雅简洁的方式,来对上面的代码进行优化,那就是使用unique_ptr:

Expression Templates

Expression Templates 是一种 C++ 模板元编程技术,它通过在编译时构建按需执行的计算表达式,从而生成高效的代码。简单来说,通过 Expression Templates,我们可以实现惰性求值和消除因为中间结果而创建的临时变量。

我们构造了一个MyVector类,并且重载了MyVector+*操作符,实现两个MyVector中相同下标元素的+*操作。 对于这样的需求我们很容易写出形如下面代码的一个简单的实现:

#include <cassert>
#include <iostream>
#include <vector>

template<typename T> class MyVector
{
public:
    MyVector(const std::size_t n)
        : vec_(n)
    {}
    MyVector(const std::size_t n, const T initvalues)
        : vec_(n, initvalues)
    {}

    std::size_t size() const { return vec_.size(); }

    T operator[](const std::size_t i) const
    {
        assert(i < size());
        return vec_[i];
    }

    T& operator[](const std::size_t i)
    {
        assert(i < size());
        return vec_[i];
    }

private:
    std::vector<T> vec_;
};

template<typename T> MyVector<T> operator+(const MyVector<T>& a, const MyVector<T>& b)
{
    assert(a.size() == b.size());
    MyVector<T> result(a.size());
    for (std::size_t i = 0; i < a.size(); ++i) {
        result[i] = a[i] + b[i];
    }
    return result;
}

template<typename T> MyVector<T> operator*(const MyVector<T>& a, const MyVector<T>& b)
{
    assert(a.size() == b.size());
    MyVector<T> result(a.size());
    for (std::size_t i = 0; i < a.size(); ++i) {
        result[i] = a[i] * b[i];
    }
    return result;
}

template<typename T> std::ostream& operator<<(std::ostream& os, const MyVector<T>& vec)
{
    std::cout << '\n';
    for (std::size_t i = 0; i < vec.size(); ++i) {
        os << vec[i] << ' ';
    }
    os << '\n';
    return os;
}

int main(int argc, char* argv[])
{
    MyVector<double> x(10, 5.4);
    MyVector<double> y(10, 10.3);
    auto             ret = x + x + y * y;
    std::cout << ret << std::endl;
    return 0;
}

这个实现平淡无奇,相信每个人都能随手写出来。在godbolt上编译成汇编来分析:

c++中的动态多态和静态多态

在 c++中为了实现多态,使用了一种动态绑定的技术,这个技术的核心就是虚函数表(virtual table)。下面就简单的说明一下基于虚表的动态绑定的原理,从而更好的与静态多态做比较。

在 c++中,每个包含虚函数的类都有一个虚表。我们来看下面这个类:

// demo.cpp
class A
{
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void         func1();
    void         func2();

private:
    int m_data1, m_data2;
};

我们可以借助编译器来查看上述类的对象布局:

c++17:constexpr if

constexpr 是 c++11 引入的关键字,用于编译时常量和常量表达式。而 c++17 将这一特性做了增强,引入了 constexpr if , 使得编译器在编译时(compile time)能够做分支判断,从而有条件的编译代码。

下面可以通过一个简单的例子来看看constexpr if的用法:

#include <iostream>
#include <type_traits>

template<typename T> auto getValue(T t)
{
    if constexpr (std::is_pointer<T>::value) {
        return *t;
    } else {
        return t;
    }
}

int main(int argc, char* argv[])
{
    int  a = 10;
    int* b = &a;
    getValue(a);
    getValue(b);
    return 0;
}

其实和普通的条件判断区别不大,只不过constexpr if中的条件是常量表达式,可以在编译时确定条件表达式的结果,从而选择编译对应的分支代码。 我们可以将上述代码编译成汇编来进一步分析:

c++20:Designated Initializers

对于熟悉 c99 的人来说,Designated Initializers 并不算是什么新鲜事物,然而 c++直到 c++20 才正式支持这一特性。 虽然在 c++20 之前,像 GCC 这样的编译器通过扩展的形式已经对该特性做了支持,但是随着 c++20 将其纳入新标准,这一特性将在所有编译器中得到支持。

Designated Initialization 是聚合初始化(Aggregate Initialization)的一种形式。 在 c++20 中,聚合类型(Aggregate types)是指:

  • 数组类型
  • 具备如下特性的 class 类型:
    • has no private or protected direct non-static data members
    • has no user-declared or inherited constructors
    • has no virtual, private, or protected base classes
    • has no virtual member functions

c++20 中的 Designated Initializers 的用法跟 c99 非常相似:

struct Points
{
    double x{0.0};
    double y{0.0};
};

const Points p{.x = 1.1, .y = 2.2};
const Points o{.x{1.1}, .y{2.2}};
const Points x{.x = 1.1, .y{2.2}};

使用 Designated Initializers 最大的好处就是能够提升代码的可读性。

Linux磁盘IO

做存储开发,必然绑定在 I/O 路径上。现代高级语言和标准库往往将底层细节封装得很好,降低了开发门槛,但也让我们对数据从应用到磁盘这条链路上的每一层缓冲缺乏清晰认知。本文从用户态到内核态,逐层梳理 Linux 磁盘 I/O 涉及的关键系统调用及其语义差异,帮助理解"数据到底什么时候才算安全落盘"。

一次典型的写操作,数据会经过以下层次:

应用程序 → C 标准库 IO Buffer → 内核 Page Cache → 磁盘控制器 Write Cache → 持久化介质

每一层缓冲都是为了提升性能而存在的,但也意味着在任何一层发生故障时,尚未向下刷写的数据都可能丢失。理解各层的边界和刷写机制,是保证数据可靠性的基础。

c++元编程之遍历tuple

对于一个标准的 c++容器来说,我们可以很容易在运行时使用迭代器和 range-based for loop 来遍历其中的每一个元素。但是对于std::tuple,却不能像普通的容器那样去遍历它。

std::tuple是一个具有固定大小,包含不同类型值的集和。与之相似的是std::pair,只不过std::pair只能容纳两个元素, 而std::tuple可以容纳许多元素:

std::tuple<int, double, const char*> tup {42, 10.5, "hello"};

// or with CTAD(class template argument deduction), C++17:
std::tuple deducedTup {42, 10.5, "hello"}; // 自动推导类型

访问std::pair中的元素只需要访问.first.second成员即可:

c++17:string_view

std::string_view是 c++17 中新增的一种类型。其核心理念是,能够让我们在传统的 C++03 风格的具体性和泛型编程之间找到一个很好的折衷点。 在 C++17 标准之前,我们通常只能在粗糙的不严谨的模板实现和相对严谨但是有着冗长约束的模板之间做出选择。举个简单的例子:

// c++03 style
class Widget
{
    std::string name_;

public:
    void setName(const char* new_name);
    void setName(const std::string& new_name);
};

// 不正确的欠约束的模板
class Widget
{
    std::string name_;

public:
    template<class T> void setName(T&& new_name);
};

// 正确的但是约束但滑稽冗长的模板
class Widget
{
    std::string name_;

public:
    template<class T, class = decltype(std::declval<std::string&>() = std::declval<T&&>()) >>
                              void setName(T&& new_name);
};

而有了 string_view 之后,以上代码就可以简化成如下: