如何在C++中实现线程安全的单例模式?

在多线程环境下,单例模式的实现尤为重要,因为它必须保证在任何时刻只有一个实例存在,并且在并发情况下不会出现竞态条件。以下是一种常用且高效的实现方式,结合了 C++11 之后的特性:std::call_oncestd::once_flag

1. 基本思路

  • 懒汉式:实例在第一次使用时才创建,避免不必要的资源占用。
  • 线程安全:利用 std::call_once 确保单次初始化,即使多个线程同时请求也只会执行一次构造。
  • 懒加载:通过局部静态变量或 std::unique_ptr 延迟初始化。

2. 代码实现

#include <iostream>
#include <mutex>
#include <memory>

class Singleton {
public:
    // 提供全局访问点
    static Singleton& Instance() {
        // std::call_once 只会执行一次
        std::call_once(initFlag_, []() {
            instance_.reset(new Singleton);
        });
        return *instance_;
    }

    // 业务方法示例
    void doSomething() const {
        std::cout << "Singleton instance address: " << this << std::endl;
    }

    // 禁止拷贝构造和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() {
        std::cout << "Singleton constructed at " << this << std::endl;
    }
    ~Singleton() = default;

    // 单例实例
    static std::unique_ptr <Singleton> instance_;
    // 用于控制一次初始化
    static std::once_flag initFlag_;
};

// 静态成员定义
std::unique_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;

关键点说明

  1. std::call_oncestd::once_flag
    std::call_once 接受一个 std::once_flag 以及一个可调用对象(lambda、函数指针等)。无论有多少线程调用 Instance()initFlag_ 只会让其中一个线程执行 lambda,保证单例唯一性。

  2. 懒加载
    `std::unique_ptr

    ` 只在 `call_once` 里实例化,避免在程序启动时就创建。
  3. 线程安全的析构
    如果你需要在程序结束时销毁单例,使用 std::unique_ptr 可以自动在全局静态对象析构时销毁实例。若你想手动控制生命周期,可在 Singleton 内部实现 destroy() 方法,使用 std::call_once 确保只销毁一次。

  4. 禁止拷贝/赋值
    为了确保唯一性,删除拷贝构造和赋值运算符。

3. 使用示例

#include <thread>

void worker() {
    Singleton::Instance().doSomething();
}

int main() {
    std::thread t1(worker);
    std::thread t2(worker);
    std::thread t3(worker);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

运行结果将显示同一实例地址,证明只有一个实例被创建。

4. 进阶:C++17 以上的局部静态变量

C++11 之后,局部静态变量的初始化已是线程安全的。因此可以更简洁地实现:

class Singleton {
public:
    static Singleton& Instance() {
        static Singleton instance;
        return instance;
    }
    // 其余同上
};

这种方式省略了 std::call_once,编译器保证线程安全。缺点是如果实例构造抛异常,后续访问会导致再次尝试初始化,可能会产生不确定行为。若构造函数不抛异常,推荐使用此方式。

5. 小结

  • 使用 std::call_oncestd::once_flag 可以在 C++11 以上安全实现线程安全单例。
  • 通过 std::unique_ptr 或局部静态变量完成懒加载与自动析构。
  • 删除拷贝/赋值以保持唯一性。
  • 对于性能敏感的场景,可考虑直接使用局部静态变量,因其在多数编译器中实现更高效。

这样,你就可以在多线程 C++ 程序中安全、简洁地使用单例模式了。

**标题:C++20 中的 constexpr 函数:从编译期计算到运行时灵活性**

在 C++20 之前,constexpr 函数仅能在编译时求值,且对函数体的限制相对严格。随着 C++20 的发布,这些限制大幅放宽,使得 constexpr 函数既能在编译期执行,也能在运行时正常调用。本文将从语法、限制、典型用法以及与常见问题的对比三方面,深入剖析 C++20 constexpr 函数的演进与应用。


1. constexpr 函数的演进

标准 关键变更 说明
C++11 仅能包含单个返回语句、有限循环、限制的指针 constexpr 主要用于编译期常量表达式
C++14 支持多语句、循环、递归 允许在编译期执行更复杂的计算
C++17 允许返回引用、支持 std::initializer_list 进一步提高可用性
C++20 允许在运行时调用 constexpr,引入 constevalconstinitconst 变量 使 constexpr 函数在编译期与运行时之间无缝切换

consteval 强制在编译期求值;constinit 确保变量在编译期初始化;const 变量在 C++20 中不再需要 constexpr 限定。


2. 语法与使用示例

2.1 基础示例

constexpr int square(int x) {
    return x * x;
}

在编译期,square(5) 可被替换为 25;在运行时,仍然可以调用。

2.2 循环与递归

constexpr int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i) result *= i;
    return result;
}

C++20 允许循环体内使用 breakcontinue 等语句,且递归深度可以在编译期无限递归,前提是满足编译器的递归深度限制。

2.3 引用返回

constexpr int& get_static_ref() {
    static int val = 42;
    return val;
}

此函数在编译期返回对静态变量的引用,在运行时也可正常使用。


3. 与 consteval 的比较

关键字 强制执行 可在编译期使用 可在运行时使用
constexpr
consteval
  • 使用场景:当你需要保证某个表达式必须在编译期求值,且不允许在运行时被调用时,选择 consteval。如编译期检查数组大小。

4. 常见问题与误区

4.1 constexpr 函数的递归深度限制

编译器对递归深度有限制(通常为 1024)。如果需要更深层次的编译期递归,可考虑改用模板元编程或 std::integral_constant

4.2 constexpr 变量与 const 的关系

C++20 将 const 变量视为 constexpr,但仅在其初始化表达式为 constexpr 时才满足。若想强制编译期初始化,可使用 constinit

constinit int x = compute(); // 必须在编译期计算

4.3 与模板的结合

constexpr 函数可以作为非类型模板参数:

template<int N>
struct S {
    static constexpr int value = N;
};

constexpr int pow2(int n) { return 1 << n; }
S<pow2(4)> s; // 等价于 S<16>

这让模板参数不再局限于整数字面量,而是可以是任意 constexpr 表达式。


5. 实战案例:编译期字符串拼接

C++20 引入了 std::string_viewconstexpr 支持,使得在编译期构造字符串变得可行。

#include <string_view>
#include <array>

constexpr std::array<char, 10> hello() {
    std::array<char, 10> arr{};
    const char* src = "Hello";
    for (int i = 0; src[i] != '\0'; ++i) {
        arr[i] = src[i];
    }
    return arr;
}

constexpr auto str = hello();
static_assert(str[0] == 'H');

此示例演示了在编译期构造字符数组,并在 static_assert 中验证结果。


6. 小结

C++20 的 constexpr 函数通过放宽语法限制和引入新的关键词,极大提升了编译期计算的能力。开发者可以更灵活地在编译期完成复杂计算、生成元数据,甚至将 constexpr 函数与模板元编程结合,构建更高效、类型安全的代码。随着编译器对 constexpr 的优化不断提升,合理使用这些特性,将使程序在性能和安全性上获得双重收益。

C++20 中的 std::span 与传统指针的比较

在 C++20 之前,C++ 代码中处理连续容器片段的方式大多是使用原始指针配合长度信息。随着 std::span 的加入,标准化了这一操作模式,提供了更安全、更易读的接口。本文将从定义、使用场景、性能以及兼容性四个方面对比 std::span 与传统指针,帮助你在项目中做出更合适的选择。


1. 基本定义

对象 关键字 语义
std::span std::span<T, N> 逻辑上等价于 T* ptrsize_t len 的组合;模板参数 N 可指定常量大小,默认为 dynamic_extent(运行时可变)
指针 T* ptr 只保存地址,缺乏长度信息

std::span 本质上是一个轻量级的“视图”对象,持有指针和长度,且本身不拥有底层数据,避免了额外的内存开销。


2. 使用场景对比

2.1 只读访问

void process(const std::span<const int>& data) {
    for (int v : data) std::cout << v << ' ';
}

相比:

void process(const int* data, std::size_t len) {
    for (std::size_t i = 0; i < len; ++i) std::cout << data[i] << ' ';
}
  • 可读性const std::span<const int>& 一眼即可知函数接受的是一个只读的整数序列。
  • 安全性:编译器可对 span 的范围进行边界检查(在某些实现中),减少越界风险。

2.2 可变访问

void increment(std::span <int> data) {
    for (int& v : data) ++v;
}

传统方式:

void increment(int* data, std::size_t len) {
    for (std::size_t i = 0; i < len; ++i) ++data[i];
}
  • 语义统一:只读与可变使用同一个模板,区别在于 `span ` 与 `span`。
  • 避免指针误用span 的引用传递可防止忘记传递长度。

2.3 与标准容器互操作

std::vector <int> vec{1,2,3,4,5};
process(std::span<const int>(vec));          // 隐式转换
process(vec);                                 // 直接传递,隐式转换为 span

传统指针则需手动取地址:

process(vec.data(), vec.size());

3. 性能与内存

std::span 的实现通常是一个包含指针和大小的 POD 结构:

struct span {
    T* ptr;
    std::size_t size;
};

这与两根指针(T* begin, T* end)或指针+长度的组合在内存占用上没有区别。拷贝成本同样极低,只有几字节。

关键的性能差异在于 函数调用的接口

  • span 可以作为参数、返回值、成员变量传递,符合现代 C++ 的设计理念。
  • 传统指针+长度组合往往导致接口繁琐,需要额外的文档说明参数顺序。

4. 兼容性与迁移

兼容性 说明
旧编译器 std::span 需要 C++20 或支持的后向兼容库(如 std::experimental::span
第三方库 大多数现代库已接受 span 作为参数类型;若使用旧库,仍可通过 T*size_t 的适配器实现
内存安全 span 本身不管理内存,仍需保证底层数据在 span 生命周期内有效

迁移时的常见做法是先引入 std::span 作为新的接口,然后为旧接口提供包装函数:

void process_old(const int* data, std::size_t len) {
    process(std::span<const int>(data, len));
}

5. 小结

  • 安全std::span 明确表示“视图”,可在 IDE/编译器层面更好地追踪错误。
  • 简洁:接口统一,减少了函数签名的冗余。
  • 性能:与传统指针+长度等价,且易于编译器优化。

在新项目或对代码可读性、维护性有较高要求的场景,建议优先使用 std::span。在需要兼容旧编译器或已有大量基于指针的 API 时,保持传统方式亦可行,且可通过包装层逐步过渡。

**C++17 中的 std::optional 与异常安全**

在 C++17 标准中,std::optional 为处理可缺失值提供了更优雅的方式。相比传统的指针或布尔标志,optional 既保持了值语义,又能显式表达“可能为空”的状态。本文将从设计理念、使用场景、与异常安全的关系三个角度展开,帮助读者深入理解并在实践中灵活运用。

一、设计理念

`std::optional

` 实际上是一个包装器,内部包含一个可能未初始化的 `T` 对象。其核心特性包括: – **无论 T 是否为非平凡类型,`optional` 只在需要时才构造 T**。这避免了不必要的默认构造或销毁。 – **提供了 `has_value()`、`value()`、`operator*()`、`operator->()` 等访问接口**,让使用者可以像使用普通对象一样操作,同时通过 `has_value()` 明确判断是否存在值。 – **符合值语义**:复制、移动、比较等操作都与 `T` 的相应操作保持一致。若 `T` 可比较,`optional ` 也可比较。 ### 二、典型使用场景 | 场景 | 传统实现 | `optional` 实现 | 优点 | |——|———-|—————–|——| | 可能为空的返回值 | `T*` 或 `std::unique_ptr ` | `std::optional` | 更短更安全,避免空指针错误 | | 可选配置参数 | `std::map` + 关键字检查 | `std::unordered_map` + `optional` | 明确区分“未提供”与“提供空值” | | 解析错误信息 | 返回 `bool` + `out` 参数 | `std::optional ` | 一体化返回,易于链式调用 | ### 三、异常安全分析 在异常安全的讨论中,`optional` 以其构造与销毁的确定性发挥重要作用。考虑以下函数: “`cpp std::optional read_file(const std::string& path) { std::ifstream in(path); if (!in) return std::nullopt; // 文件不可读,返回空值 std::ostringstream buf; buf > get_resource() { if (!condition) return std::nullopt; return std::make_unique (); } “` 此时,若构造 `Resource` 时抛异常,`unique_ptr` 会自动析构,避免泄漏。 ### 四、实战示例:链式配置解析 下面给出一个简易的配置解析器,展示 `optional` 在异常安全与链式调用中的实用性。 “`cpp #include #include #include #include class Config { public: std::optional timeout; std::optional host; std::optional use_ssl; }; std::optional parse_config(const std::unordered_map& kv) { Config cfg; try { if (auto it = kv.find(“timeout”); it != kv.end()) cfg.timeout = std::stoi(it->second); if (auto it = kv.find(“host”); it != kv.end()) cfg.host = it->second; if (auto it = kv.find(“use_ssl”); it != kv.end()) cfg.use_ssl = (it->second == “true”); return cfg; } catch (…) { // 若任何转换失败,返回空值 return std::nullopt; } } “` 使用时: “`cpp int main() { std::unordered_map kv = { {“timeout”, “30”}, {“host”, “example.com”}, {“use_ssl”, “true”} }; if (auto cfg_opt = parse_config(kv); cfg_opt) { const auto& cfg = *cfg_opt; std::cout

C++20 Concepts:提高模板编程的安全性与可读性

在 C++20 中引入了 Concepts(概念)这一强大的特性,它为模板编程提供了更直观、更安全、更易维护的语法。本文将从概念的基本语法、实际使用场景、以及如何在现有项目中逐步迁移来介绍这一特性。

1. 什么是 Concepts?

Concepts 是对模板参数进行约束的一种方式。它相当于对类型进行“标签”或“接口”,要求传入的类型必须满足特定的属性(如存在某个成员函数、支持某个运算符等)。如果不满足,则编译器会给出更友好的错误信息,而不是一连串无关紧要的模板实例化错误。

2. 基本语法

2.1 定义概念

#include <concepts>

template <typename T>
concept Incrementable = requires(T a) {
    { ++a } -> std::same_as<T&>;
    { a++ } -> std::same_as <T>;
};

上述 Incrementable 约束确保类型 T 支持前置递增和后置递增,并且递增后的结果类型符合预期。

2.2 在模板中使用

template <Incrementable T>
T add_one(T x) {
    return ++x;
}

如果尝试传递不满足 Incrementable 的类型,编译器会报错:

error: template argument deduction/substitution failed

而不是一堆难以理解的错误。

3. 实际案例

3.1 容器元素可迭代性

#include <ranges>
#include <vector>
#include <iostream>

template <std::ranges::input_range R>
void print_range(const R& r) {
    for (const auto& val : r) {
        std::cout << val << ' ';
    }
    std::cout << '\n';
}

int main() {
    std::vector <int> v = {1, 2, 3};
    print_range(v);          // OK
    std::string s = "abc";
    print_range(s);          // OK
    // int x = 5;
    // print_range(x);        // 编译错误,int 不是输入范围
}

3.2 函数对象约束

#include <concepts>

template <typename F, typename Arg>
concept Invocable = requires(F f, Arg a) {
    { f(a) } -> std::convertible_to<std::invoke_result_t<F, Arg>>;
};

template <Invocable F, typename Arg>
auto call_and_double(F f, Arg a) {
    return 2 * f(a);
}

这里 Invocable 确保 f 可以被调用并返回一个可以与 int 兼容的结果。

4. 与传统 SFINAE 的对比

特性 Concepts SFINAE
可读性
错误信息 明确 混乱
约束表达 简洁 复杂
递归约束 直观 难以维护

虽然 SFINAE 仍然可以使用,但在现代 C++20 代码中,推荐使用 Concepts 来替代复杂的模板元编程。

5. 在已有项目中的迁移

  1. 识别热点模板:先定位那些对调用方类型约束不明确导致错误的模板函数或类。
  2. 定义概念:为这些热点模板编写概念,覆盖必要的成员函数、运算符或属性。
  3. 改写模板:使用概念替换旧的 typenameclass 模板参数,添加 requires 子句或直接放在参数列表前。
  4. 测试:运行单元测试,确保新约束不会意外导致合法调用失效。

6. 小结

Concepts 为 C++ 模板编程带来了前所未有的安全性和可读性。通过明确的约束,开发者可以在编译期捕获错误,减少调试时间,并让代码更加自文档化。随着编译器对 Concepts 的优化,实际运行性能也不再受牵涉,完全可以放心迁移到 C++20+。祝你在 C++ 模板世界里玩得开心!

C++ 中的 constexpr 与 consteval 区别与应用

在 C++20 之前,constexpr 关键字已经为我们提供了在编译期求值的能力,但它并不强制在编译期求值,更多的是一种“建议”。从 C++20 开始引入的 consteval 则彻底把函数的求值时机锁定在编译期,任何运行时调用都会导致编译错误。下面我们分别梳理两者的语义差异、适用场景,并给出实际代码示例,帮助开发者在实践中更好地选择使用。

1. 语义对比

关键字 编译期求值强制性 运行时是否允许 适用对象
constexpr 允许,但不强制 允许 函数、变量、构造函数等
consteval 强制 不允许 函数(不能声明为 consteval 的变量)
  • constexpr:如果函数或变量在使用时能在编译期求值,编译器会尝试这么做;如果不行,则退回到运行时。此时编译器并不会报错,但会在使用处出现运行时计算。
  • consteval:编译器必须在编译期求值,若不能,则编译失败。编译器在遇到 consteval 函数调用时,任何使用都被强制要求在编译期完成。

2. 实际使用场景

场景 推荐关键字 说明
需要在编译期计算常量,并希望编译器在必要时退回到运行时 constexpr 例如模板元编程中的阶乘,某些值不一定能在编译期确定。
必须保证函数在编译期求值,以便其结果用于 constexpr 变量或 static_assert consteval 用来构造真正的编译期常量,防止误用。
想在编译期检测某些属性但不需要返回值 constexpr + static_assert 通过 constexpr 函数返回 true/false,再在 static_assert 中使用。
编译期数组大小 constexpr 传统用法,返回数组大小。

3. 代码示例

3.1 constexpr 计算阶乘

constexpr unsigned long long factorial(unsigned int n) {
    return n <= 1 ? 1ULL : n * factorial(n - 1);
}

constexpr unsigned long long fact5 = factorial(5); // 120 在编译期求值
static_assert(fact5 == 120, "阶乘错误");

3.2 consteval 强制编译期

consteval int compute(int x) {
    return x * 2;   // 必须在编译期求值
}

constexpr int y = compute(10);   // 正常
int z = compute(10);             // 编译错误:consteval 函数不能在运行时调用

3.3 constevalconstexpr 混合使用

constexpr int square(int x) { return x * x; }   // 允许编译期或运行时

consteval int cube(int x) { return x * x * x; } // 必须编译期

constexpr int s = square(5);          // 25 在编译期求值
static_assert(cube(3) == 27, "立方错误"); // 编译期检查

4. 常见陷阱

  1. constexpr 变量不一定是常量表达式
    由于 constexpr 变量本身是常量,但其初始化表达式若不是常量表达式,编译器会退回到运行时。例如:

    constexpr int a = std::rand(); // 编译器报错:rand 不是常量表达式
  2. consteval 不能作为类成员
    consteval 只能修饰自由函数,不能修饰成员函数或构造函数。

  3. 编译器实现差异
    对于 constexpr,不同编译器在推导时机上的实现可能略有差异,尤其是 C++17 之前的标准允许更宽松的求值策略。

5. 结语

  • 若你只需要 可能 在编译期求值,或者想让编译器在不支持时退回到运行时,使用 constexpr
  • 若你想强制编译期求值,避免任何运行时调用,或者在 static_assert 等上下文中使用,选择 consteval

掌握两者的区别与适用场景,可以让你在写高效、可靠的 C++ 代码时更加游刃有余。

**如何在 C++ 中实现一个线程安全的懒汉式单例?**

在多线程环境下,单例模式的实现往往会遇到并发安全问题。下面以 C++17 为例,介绍几种常用且线程安全的实现方式,并说明各自的优缺点。


1. Meyers 单例(局部静态变量)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // C++11 之后线程安全
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
};
  • 优点

    • 代码简洁,编译器负责初始化顺序。
    • C++11 引入的局部静态变量初始化是线程安全的,避免了显式锁。
  • 缺点

    • 延迟初始化:如果程序未访问 instance(),对象永不创建。
    • 对于类构造失败时的异常处理,只有在第一次访问时才会抛出。

2. 双重检查锁(Double-Checked Locking)

class Singleton {
public:
    static Singleton* getInstance() {
        Singleton* tmp = instance_.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mutex_);
            tmp = instance_.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new Singleton();
                instance_.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
  • 优点

    • 仅在第一次创建时使用锁,后续访问无锁开销。
    • 适用于需要手动销毁或在全局析构顺序中控制的情况。
  • 缺点

    • 代码复杂,容易出现细微错误(如内存序问题)。
    • 在 C++11 之前,双重检查锁并不保证线程安全。

3. 经典的 std::call_once

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, []() { ptr_ = new Singleton(); });
        return *ptr_;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
    static Singleton* ptr_;
    static std::once_flag flag_;
};

Singleton* Singleton::ptr_ = nullptr;
std::once_flag Singleton::flag_;
  • 优点

    • 代码可读性好,内部使用 std::once_flag 保障一次性执行。
    • 适合需要显式销毁或在某些平台上控制初始化顺序的情况。
  • 缺点

    • 仍然需要手动管理内存,若不销毁会造成内存泄漏。
    • 对比 Meyers 单例,稍有性能损耗(一次锁判断)。

4. std::shared_ptr 结合 std::weak_ptr

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (!ptr_) {
            ptr_ = std::shared_ptr <Singleton>(new Singleton(), [](Singleton*){});
        }
        return ptr_;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
    static std::weak_ptr <Singleton> ptr_;
    static std::mutex mutex_;
};

std::weak_ptr <Singleton> Singleton::ptr_;
std::mutex Singleton::mutex_;
  • 优点

    • 通过 std::weak_ptr 可以在需要时检测实例是否已被销毁。
    • 自动内存管理,避免手动 delete
  • 缺点

    • 仍然需要显式锁。
    • weak_ptr 的使用稍显冗余,除非需要监控实例生命周期。

小结

  • 最推荐:Meyers 单例(局部静态变量)——最简洁、线程安全、几乎无性能损耗。
  • 需要显式销毁std::call_oncestd::shared_ptr/std::weak_ptr 方案。
  • 高性能需求:双重检查锁,但必须确保正确使用原子和内存序。

在实际项目中,除非你有特殊需求(如需要在多线程程序退出前手动销毁单例),否则建议使用第一种方法,以保持代码的简洁与安全。

如何在 C++ 中实现自定义线程安全的智能指针

在 C++ 中,智能指针是管理资源生命周期的重要工具。标准库已经提供了 std::unique_ptrstd::shared_ptr 等实现,但在某些场景下,可能需要自己实现一个支持多线程安全的引用计数智能指针。下面将从设计思路、核心实现到使用示例,逐步介绍如何编写一个简易但线程安全的 MySharedPtr


1. 设计目标

功能 说明
引用计数 使用 std::atomic<std::size_t> 维护计数,避免锁
线程安全 对计数的增减均为原子操作,构造/析构时无数据竞争
自定义删除器 支持传入删除器,用于自定义资源释放方式
可复制/可移动 复制时计数递增,移动后源对象失效
弱引用 提供 MyWeakPtr,支持 lock() 获取有效指针

2. 基础实现

#include <atomic>
#include <utility>
#include <iostream>

template <typename T>
class MySharedPtr;

template <typename T>
class MyWeakPtr;

template <typename T>
class ControlBlock {
public:
    std::atomic<std::size_t> use_count{1};
    std::atomic<std::size_t> weak_count{0};
    T* ptr;
    void (*deleter)(T*) = nullptr;

    ControlBlock(T* p, void (*del)(T*) = nullptr)
        : ptr(p), deleter(del) {}

    void release() {
        if (ptr && deleter) deleter(ptr);
        delete ptr;
    }
};

template <typename T>
class MySharedPtr {
public:
    MySharedPtr() noexcept : cb(nullptr) {}

    explicit MySharedPtr(T* ptr, void (*del)(T*) = nullptr)
        : cb(ptr ? new ControlBlock <T>(ptr, del) : nullptr) {}

    MySharedPtr(const MySharedPtr& other) noexcept
        : cb(other.cb) {
        increment();
    }

    MySharedPtr(MySharedPtr&& other) noexcept
        : cb(other.cb) {
        other.cb = nullptr;
    }

    ~MySharedPtr() {
        decrement();
    }

    MySharedPtr& operator=(const MySharedPtr& other) noexcept {
        if (this != &other) {
            decrement();
            cb = other.cb;
            increment();
        }
        return *this;
    }

    MySharedPtr& operator=(MySharedPtr&& other) noexcept {
        if (this != &other) {
            decrement();
            cb = other.cb;
            other.cb = nullptr;
        }
        return *this;
    }

    T* get() const noexcept { return cb ? cb->ptr : nullptr; }
    T& operator*() const noexcept { return *get(); }
    T* operator->() const noexcept { return get(); }
    std::size_t use_count() const noexcept {
        return cb ? cb->use_count.load(std::memory_order_acquire) : 0;
    }
    bool unique() const noexcept { return use_count() == 1; }

    // For weak pointer creation
    friend class MyWeakPtr <T>;

private:
    ControlBlock <T>* cb;

    void increment() {
        if (cb) cb->use_count.fetch_add(1, std::memory_order_acq_rel);
    }

    void decrement() {
        if (cb) {
            if (cb->use_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
                cb->release();
                if (cb->weak_count.load(std::memory_order_acquire) == 0) {
                    delete cb;
                }
            }
        }
    }
};

template <typename T>
class MyWeakPtr {
public:
    MyWeakPtr() noexcept : cb(nullptr) {}

    MyWeakPtr(const MySharedPtr <T>& shared) noexcept
        : cb(shared.cb) {
        increment();
    }

    MyWeakPtr(const MyWeakPtr& other) noexcept
        : cb(other.cb) {
        increment();
    }

    MyWeakPtr(MyWeakPtr&& other) noexcept
        : cb(other.cb) {
        other.cb = nullptr;
    }

    ~MyWeakPtr() {
        decrement();
    }

    MyWeakPtr& operator=(const MyWeakPtr& other) noexcept {
        if (this != &other) {
            decrement();
            cb = other.cb;
            increment();
        }
        return *this;
    }

    MyWeakPtr& operator=(MyWeakPtr&& other) noexcept {
        if (this != &other) {
            decrement();
            cb = other.cb;
            other.cb = nullptr;
        }
        return *this;
    }

    MySharedPtr <T> lock() const noexcept {
        if (!cb) return MySharedPtr <T>();
        std::size_t curr = cb->use_count.load(std::memory_order_acquire);
        while (curr != 0) {
            if (cb->use_count.compare_exchange_weak(
                    curr, curr + 1,
                    std::memory_order_acq_rel,
                    std::memory_order_acquire)) {
                return MySharedPtr <T>(cb);
            }
        }
        return MySharedPtr <T>();
    }

    bool expired() const noexcept {
        return !cb || cb->use_count.load(std::memory_order_acquire) == 0;
    }

    std::size_t use_count() const noexcept {
        return cb ? cb->use_count.load(std::memory_order_acquire) : 0;
    }

private:
    ControlBlock <T>* cb;

    void increment() {
        if (cb) cb->weak_count.fetch_add(1, std::memory_order_acq_rel);
    }

    void decrement() {
        if (cb) {
            if (cb->weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
                if (cb->use_count.load(std::memory_order_acquire) == 0) {
                    delete cb;
                }
            }
        }
    }
};

关键点说明

  1. 原子引用计数
    std::atomic<std::size_t> 保证了在多线程环境下计数的正确性,无需加锁。

  2. 删除器
    默认使用 delete,但用户可以传入自定义删除器,适用于 malloc/freenew[]/delete[] 或自定义资源。

  3. 弱指针实现
    MyWeakPtr 与标准库实现保持一致,支持 lock() 获取强引用,并且在引用计数归零后才释放控制块。


3. 使用示例

#include <thread>
#include <vector>

struct Resource {
    int id;
    Resource(int x) : id(x) { std::cout << "Resource " << id << " constructed\n"; }
    ~Resource() { std::cout << "Resource " << id << " destructed\n"; }
};

int main() {
    MySharedPtr <Resource> sp1(new Resource(42));
    std::cout << "use_count: " << sp1.use_count() << '\n'; // 1

    {
        MySharedPtr <Resource> sp2 = sp1;
        std::cout << "After copy, use_count: " << sp1.use_count() << '\n'; // 2

        MyWeakPtr <Resource> wp = sp1;
        std::cout << "Weak use_count: " << wp.use_count() << '\n'; // 2

        // 多线程访问
        std::vector<std::thread> threads;
        for (int i = 0; i < 5; ++i) {
            threads.emplace_back([sp1]() {
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
                std::cout << "Thread got id: " << sp1->id << '\n';
            });
        }
        for (auto& t : threads) t.join();
    }

    std::cout << "After block, use_count: " << sp1.use_count() << '\n'; // 1
    // 当 sp1 失效时,Resource 会被析构
    return 0;
}

运行结果示例:

Resource 42 constructed
use_count: 1
After copy, use_count: 2
Weak use_count: 2
Thread got id: 42
Thread got id: 42
Thread got id: 42
Thread got id: 42
Thread got id: 42
After block, use_count: 1
Resource 42 destructed

4. 进一步优化

优化方向 说明
内存池 ControlBlock 与对象共置,减少分配次数
锁优化 对于读多写少场景,可使用 std::shared_mutex 与读写锁
自定义内存分配器 通过模板参数注入分配器,兼容多种内存管理策略
异常安全 在构造时若分配失败抛异常,确保没有泄漏

小结

通过上述实现,我们获得了一个轻量、线程安全、支持自定义删除器的智能指针。虽然标准库已经提供了 std::shared_ptrstd::weak_ptr,但在需要对内部细节做特殊控制或嵌入自定义资源管理时,自定义实现可以提供更大的灵活性。希望本文能帮助你更深入地理解智能指针的内部工作原理,并为自己的项目提供参考。

C++17 的 Structured Bindings 在现代编程中的应用

在 C++17 中引入的 Structured Bindings 为我们处理复合类型(如 std::tuple、std::pair、array 等)带来了极大的便利。它使得从容器或返回值中提取多个元素的代码变得更简洁、更可读。本文将从基本语法、常见使用场景以及性能考虑三个角度,深入探讨 Structured Bindings 在现代 C++ 编程中的实际应用,并结合实例展示其优越性。

1. 基本语法

Structured Bindings 的核心语法是使用 auto(或指定类型)加上方括号来解构对象:

auto [a, b] = some_pair;          // 解构 std::pair
auto [x, y, z] = some_tuple;      // 解构 std::tuple
auto &[s, i] = some_array;        // 解构 std::array,带引用

关键点:

  • 必须是可解构类型:std::pairstd::tuplestd::arraystruct(满足结构化绑定规则)。
  • 可以指定 auto 或明确类型。若需要引用,使用 auto&
  • 变量名顺序对应原始类型的元素顺序。

2. 常见使用场景

2.1 解构返回值

许多函数返回包含多个值的 std::tuplestd::pair,以前需要访问 `get

()`、`get()` 等,代码冗长且易错。Structured Bindings 让返回值的使用更直观: “`cpp std::tuple getInfo() { return {42, 3.14, “hello”}; } auto [id, pi, msg] = getInfo(); std::cout mp{{1, “one”}, {2, “two”}}; for (auto [key, val] : mp) { std::cout ” arr{1, 2, 3}; auto &[a, b, c] = arr; b *= 10; // 修改第二个元素 struct Point { double x, y, z; }; Point p{1.0, 2.0, 3.0}; auto [px, py, pz] = p; // 只读解构 “` ## 3. 性能与副作用 ### 3.1 是否产生拷贝 – 对于 `auto`(非引用),解构会**复制**对应元素。若元素较大或复杂,建议使用 `auto&` 或 `const auto&`。 – 对于返回 `std::tuple` 的函数,使用 `auto [a,b] = getTuple();` 时,会把整个 tuple 拷贝一次再解构,导致一次额外拷贝。若不想拷贝,使用 `auto&& [a,b] = getTuple();` 或直接解构返回值:`auto&& [a,b] = std::move(getTuple());`。 ### 3.2 与传统访问方式比较 传统 `std::get (tuple)` 方式更显式,适用于对拷贝与移动有严格要求的场景。Structured Bindings 更适合代码可读性与简洁性优先。 ## 4. 实战案例:基于键值对的缓存系统 下面给出一个简化的缓存实现,利用 Structured Bindings 处理内部数据结构: “`cpp #include #include #include class Cache { std::unordered_map store_; public: void set(const std::string& key, int value) { store_[key] = value; } bool get(const std::string& key, int& out) const { auto it = store_.find(key); if (it != store_.end()) { out = it->second; return true; } return false; } void debug() const { for (auto [k, v] : store_) { std::cout ” struct tuple_size : std::integral_constant {}; template struct tuple_element : std::conditional_t> {}; } template constexpr auto get(RGB& rgb) { if constexpr (I == 0) return rgb.r; else if constexpr (I == 1) return rgb.g; else return rgb.b; } “` 随后即可: “`cpp RGB col{255, 128, 64}; auto [r,g,b] = col; std::cout

使用 C++20 std::variant 进行类型安全的访问与调试

在 C++20 之前,处理多种可能类型的数据往往要用 boost::variant 或自己实现类似的类型安全包装。随着标准库引入 std::variant,我们可以在编译期就确定多态类型集合,既保证了类型安全,又能在运行时轻松切换和访问。本文将从 std::variant 的基本使用、访问方式、错误处理以及调试技巧四个方面,详细阐述如何利用它实现高效、可维护的代码。

1. 基础语法与构造

#include <variant>
#include <string>
#include <iostream>

using Value = std::variant<int, double, std::string>;

int main() {
    Value v1 = 42;               // 整型
    Value v2 = 3.14;             // 双精度浮点
    Value v3 = std::string("hello"); // 字符串

    std::cout << v1 << '\n';
}
  • 默认构造:如果 Value 中没有 std::monostate,必须显式初始化,否则编译错误。
  • 类型列表:使用 std::variant<Ts...> 传入一组可互斥的类型。
  • 拷贝与移动variant 支持拷贝构造、移动构造、赋值,满足 C++ 通用容器的行为。

2. 访问方式

2.1 std::get

try {
    int i = std::get <int>(v1);   // 若 v1 不是 int 则抛异常 std::bad_variant_access
} catch (const std::bad_variant_access&) {
    std::cerr << "类型不匹配\n";
}
  • 优点:直接按类型获取。
  • 缺点:若类型不匹配抛异常,必须捕获或使用 std::holds_alternative 先检查。

2.2 std::get_if

if (auto p = std::get_if <double>(&v2)) {
    std::cout << "double: " << *p << '\n';
}
  • 返回指针:若类型不匹配,返回 nullptr,无异常抛出。

2.3 std::visit

std::visit([](auto&& arg){
    std::cout << arg << '\n';
}, v3);
  • 通用访问:适用于多种类型的统一处理。
  • 可传递自定义 visitor:如 struct Visitor { void operator()(int) {...} void operator()(double) {...} ... };

3. 典型场景举例

3.1 表达式求值树

struct Add {
    std::variant<int, double> lhs, rhs;
};

struct Subtract { ... };
using Expr = std::variant<Add, Subtract, int, double>;

double eval(const Expr& e) {
    return std::visit(overloaded{
        [](int i) { return static_cast <double>(i); },
        [](double d) { return d; },
        [](const Add& a) { return eval(a.lhs) + eval(a.rhs); },
        [](const Subtract& s) { return eval(s.lhs) - eval(s.rhs); }
    }, e);
}

3.2 事件系统

struct KeyEvent { char key; };
struct MouseEvent { int x, y; };
using Event = std::variant<KeyEvent, MouseEvent>;

void handle(const Event& e) {
    std::visit([](auto&& ev){
        using T = std::decay_t<decltype(ev)>;
        if constexpr (std::is_same_v<T, KeyEvent>) {
            std::cout << "Key: " << ev.key << '\n';
        } else if constexpr (std::is_same_v<T, MouseEvent>) {
            std::cout << "Mouse: (" << ev.x << "," << ev.y << ")\n";
        }
    }, e);
}

4. 调试技巧

  1. 打印当前索引
    std::cout << v.index() << '\n'; // 0-based 索引,配合 type()

  2. 使用 std::visit 打印所有类型

    std::visit([](auto&& arg){ std::cout << arg << '\n'; }, v);
  3. **借助 `std::variant_size_v

    `** 预先验证访问索引合法性。
  4. 在断言中检查类型
    `assert(std::holds_alternative

    (v));`

5. 性能与注意事项

  • 内存占用variant 的大小为 max(sizeof(Ts...)) + alignof(max(Ts...))。若类型差异较大,可考虑 std::any
  • 构造成本:每次切换类型时需要复制/移动目标类型对象,避免频繁切换或使用 `std::optional ` 作为内部容器。
  • 异常安全std::visit 的 visitor 必须满足异常安全,若有可能抛异常,建议在 visitor 内部捕获。

6. 结语

std::variant 为 C++ 提供了强大的类型安全多态容器,既能避免传统 union 的不安全,也能取代第三方 boost::variant 的繁琐。通过正确使用 get, get_if, visit 等 API,我们可以编写既简洁又健壮的代码。熟悉 std::variant 的各种技巧,将大大提升 C++ 开发者在复杂类型交互场景下的效率和代码质量。