如何在 C++20 中使用 std::format 打印表格?

在 C++20 标准中引入的 std::format 为字符串格式化提供了类似 Python f-strings 的语法,让打印表格变得既简洁又强大。下面从基础语法、宽度对齐、列分隔符以及自定义格式化器等角度,逐步演示如何使用 std::format 构造多行、多列的表格。


1. 基础语法回顾

std::format 的调用方式类似:

#include <format>
#include <string>

std::string s = std::format("{:>10} | {:>5}", name, age);
  • {} 内可以放置格式说明符,例如 > 表示右对齐,数字表示宽度,.2f 表示浮点数保留两位小数等。

小贴士std::format 需要 C++20 编译器支持,若使用 GCC 或 Clang 需要开启 -std=c++20 选项。


2. 打印简单表格

假设我们想打印学生成绩表格,包含姓名、学号和分数三列:

#include <format>
#include <vector>
#include <iostream>

struct Student {
    std::string name;
    int id;
    double score;
};

int main() {
    std::vector <Student> students = {
        {"张三", 1001, 95.3},
        {"李四", 1002, 88.75},
        {"王五", 1003, 72.4}
    };

    // 打印表头
    std::cout << std::format("{:10} | {:>5} | {:>7}\n", "姓名", "学号", "分数");
    std::cout << std::string(10, '-') << "-+-" << std::string(5, '-') << "-+-" << std::string(7, '-') << '\n';

    // 打印数据行
    for (const auto& s : students) {
        std::cout << std::format("{:10} | {:>5} | {:>7.2f}\n", s.name, s.id, s.score);
    }
}

运行结果:

姓名       |  学号 |   分数
----------+-------+-------
张三      | 1001 | 95.30
李四      | 1002 | 88.75
王五      | 1003 | 72.40

3. 动态列宽与自适应格式

如果列宽不是固定的,而是根据内容自动决定,可以先遍历一次数据,计算最大宽度:

size_t name_w = std::strlen("姓名");
size_t id_w = std::strlen("学号");
size_t score_w = std::strlen("分数");

for (const auto& s : students) {
    name_w = std::max(name_w, s.name.size());
    id_w = std::max(id_w, std::to_string(s.id).size());
    score_w = std::max(score_w, static_cast <size_t>(std::to_string(s.score).size()));
}

然后在 std::format 中使用这些宽度:

auto header = std::format("{:<{}} | {:>{}} | {:>{}.2f}\n", "姓名", name_w, "学号", id_w, "分数", score_w);

{:<{}} 说明:左对齐,占用 name_w 个字符。


4. 通过列分隔符绘制更精美的表格

如果想让表格更像 Markdown 或 Markdown 风格的表格,可以使用 |- 分隔符,并在表头下面加一行连字符:

// 生成分隔行
std::string sep = std::string(name_w, '-') + "-+-" + std::string(id_w, '-') + "-+-" + std::string(score_w, '-');

// 打印
std::cout << header;
std::cout << sep << '\n';
for (const auto& s : students) {
    std::cout << std::format("{:<{}} | {:>{}} | {:>{}.2f}\n", s.name, name_w, s.id, id_w, s.score, score_w);
}

5. 自定义格式化器(可选)

如果你想让表格中的数字以千位分隔符显示,或者把分数转换成等级(如 A/B/C),可以实现一个自定义格式化器:

#include <format>
#include <locale>

template<typename T>
struct thousand_separator {
    T value;
};

template<typename T>
struct std::formatter<thousand_separator<T>> : std::formatter<T> {
    template<typename FormatContext>
    auto format(thousand_separator <T> s, FormatContext ctx) {
        std::ostringstream oss;
        oss.imbue(std::locale("en_US.UTF-8")); // 使用千位分隔符
        oss << s.value;
        return std::formatter <T>::format(oss.str(), ctx);
    }
};

int main() {
    std::cout << std::format("{:10}", thousand_separator{1234567}) << '\n'; // 输出 1,234,567
}

将其应用到表格即可:

std::cout << std::format("{:<{}} | {:>{}} | {:>{}.2f}\n",
                         s.name, name_w,
                         thousand_separator{s.id}, id_w,
                         s.score, score_w);

6. 小结

  • std::format 让字符串格式化变得灵活,可直接在花括号内指定对齐、宽度、精度等。
  • 通过遍历一次数据计算最大列宽,可生成自适应宽度的表格。
  • 使用 |- 等字符即可轻松构造 Markdown 风格表格。
  • 自定义格式化器可进一步提升表格显示效果(千位分隔符、分数等级转换等)。

掌握这些技巧后,你就可以在 C++20 项目中快速生成清晰、可读性高的控制台表格,提升调试与报告打印的效率。

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

在现代 C++ 开发中,单例模式经常被用于需要全局唯一实例的场景,例如日志系统、配置管理器或数据库连接池。实现线程安全的单例模式时,需要确保在多线程环境下只有一次实例化,且不产生竞争条件或性能瓶颈。以下几种实现方式可以满足这些需求,并兼顾可读性和可维护性。

1. 局部静态变量(C++11 之后的线程安全初始化)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance; // C++11 之后保证线程安全
        return instance;
    }

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

    // 示例方法
    void doSomething() { /* ... */ }

private:
    Singleton() {}   // 私有构造函数
};

优点

  • 简洁:代码只有几行,易于维护。
  • 线程安全:C++11 及以后标准保证对 static 局部变量的初始化是线程安全的。
  • 延迟初始化:实例在第一次使用时才创建,避免不必要的资源占用。

缺点

  • 初始化时机不确定:如果需要在程序入口时就初始化,需手动调用 instance()
  • 无法控制析构顺序:全局对象的析构顺序不确定,可能导致某些全局资源提前释放。

2. 带双重检查锁(双重检验锁)+ std::call_once

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag_, []() { instance_.reset(new Singleton()); });
        return *instance_;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() {}
    static std::unique_ptr <Singleton> instance_;
    static std::once_flag initFlag_;
};

std::unique_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::initFlag_;

优点

  • 明确的初始化顺序:可以在 instance() 之外的地方调用,保证在任何地方都能安全使用。
  • 只进行一次初始化std::call_once 只会执行一次,避免多次竞争。

缺点

  • 略显冗长:相比局部静态变量多几行代码。
  • 微小的性能开销:每次调用 instance() 都会检查 std::once_flag,但这个开销已被优化到极低。

3. 传统互斥锁 + 懒汉式(线程安全)

class Singleton {
public:
    static Singleton& instance() {
        std::lock_guard<std::mutex> lock(mtx_);
        if (!instance_) {
            instance_.reset(new Singleton());
        }
        return *instance_;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() {}
    static std::unique_ptr <Singleton> instance_;
    static std::mutex mtx_;
};

std::unique_ptr <Singleton> Singleton::instance_;
std::mutex Singleton::mtx_;

优点

  • 兼容旧标准:适用于 C++11 之前的编译器。
  • 易于理解:使用 mutex 进行同步,直观。

缺点

  • 性能影响:每次调用 instance() 都会锁定 mutex,导致高并发下性能下降。
  • 更易产生死锁:若在构造函数中再次访问 instance(),可能导致自旋死锁。

4. Meyers 单例(静态局部变量 + std::shared_ptr

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        static std::shared_ptr <Singleton> instance(new Singleton());
        return instance;
    }

    // ...
private:
    Singleton() {}
};

优点

  • 自动析构shared_ptr 的析构函数会在程序退出时调用,避免手动管理。
  • 灵活共享:可以将 instance() 返回值复制给其他对象,方便共享。

缺点

  • 额外的引用计数开销:虽然一般 negligible,但在极高并发下可能产生不必要的开销。

5. 原子指针 + 延迟初始化(无锁实现)

class Singleton {
public:
    static Singleton& instance() {
        Singleton* tmp = instance_.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mtx_);
            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() {}
    static std::atomic<Singleton*> instance_;
    static std::mutex mtx_;
};

std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mtx_;

优点

  • 无锁访问:在已经实例化后,访问不需要锁。
  • 细粒度控制:可根据需要选择内存顺序。

缺点

  • 实现复杂:需要仔细处理内存顺序和双重检查。
  • 潜在的 ABA 问题:若单例被销毁后再次创建,需额外注意。

6. 结合 std::shared_ptrstd::call_once(最佳实践)

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag_, []() {
            instance_ = std::make_shared <Singleton>();
        });
        return *instance_;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() {}
    static std::shared_ptr <Singleton> instance_;
    static std::once_flag initFlag_;
};

std::shared_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::initFlag_;

适用场景

  • 需要在多线程中安全初始化,且想要 shared_ptr 自动管理生命周期。
  • 兼顾性能与可读性,适合大多数项目。

7. 线程安全的延迟销毁

有时单例在程序退出前会被销毁,导致访问已销毁对象。为避免此问题,可以使用 std::atexitstd::shared_ptr 的自定义删除器。示例:

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag_, []() {
            instance_ = std::shared_ptr <Singleton>(new Singleton(), 
                [](Singleton* p){ delete p; std::cout << "Singleton destroyed\n"; });
        });
        return *instance_;
    }

private:
    Singleton() {}
    static std::shared_ptr <Singleton> instance_;
    static std::once_flag initFlag_;
};

8. 单例的单元测试注意事项

  • 避免全局状态污染:在测试前后手动重置单例。
  • 使用 std::unique_ptr 重置:在测试框架中调用 Singleton::reset()(自定义方法)来重新初始化。
  • 多线程测试:使用 std::thread 并行调用 instance(),验证同一实例被共享。

结语

在现代 C++ 中,最推荐的实现方式是使用 C++11 之后的局部静态变量std::call_oncestd::shared_ptr 的组合。它们提供了极简的语法、保证线程安全,并且对性能影响最小。无论你选择哪种实现,都建议:

  1. 删除拷贝构造与赋值操作,确保实例唯一。
  2. 使用 static 成员或局部静态来实现延迟初始化。
  3. 在多线程场景下进行彻底的并发测试,以排除竞争问题。

通过上述技巧,你可以在任何 C++ 项目中安全、简洁地实现单例模式。

**Understanding C++ Coroutines: A Modern Approach to Asynchronous Programming**

C++20 introduced coroutines as a language‑level feature that allows developers to write asynchronous code in a sequential style. A coroutine is a function that can suspend execution (co_await, co_yield, or co_return) and resume later, making it ideal for lazy evaluation, pipelines, and non‑blocking I/O. Below we explore the core concepts, a simple implementation, and best practices.


1. Coroutine Basics

Term Meaning
co_await Suspends until the awaited expression completes.
co_yield Produces a value to the caller and suspends.
co_return Returns a final value and ends the coroutine.
promise_type Provides the machinery that the compiler uses to manage coroutine state.
awaitable Any type that can be awaited; it must provide await_ready(), await_suspend(), and await_resume().

When you write a coroutine, the compiler generates a state machine that manages these suspensions. The return type of the coroutine is typically a specialization of std::future, std::generator, or a user‑defined type that hides the promise.


2. A Simple generator Example

Below is a minimal implementation of a generator that yields Fibonacci numbers. This demonstrates how to create an awaitable type and how to use co_yield.

#include <coroutine>
#include <iostream>
#include <optional>

template <typename T>
class generator {
public:
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;

    generator(handle_type h) : handle(h) {}
    ~generator() { if (handle) handle.destroy(); }

    // Iterator interface
    struct iterator {
        handle_type h;
        iterator(handle_type h_) : h(h_) { advance(); }
        iterator& operator++() { advance(); return *this; }
        const T& operator*() const { return *h.promise().current_value; }
        bool operator==(std::default_sentinel_t) const { return !h || h.done(); }

    private:
        void advance() { if (!h.done()) h.resume(); }
    };

    iterator begin() { return iterator(handle); }
    std::default_sentinel_t end() { return {}; }

private:
    handle_type handle;
};

template <typename T>
struct generator <T>::promise_type {
    std::optional <T> current_value;
    generator get_return_object() { return generator{handle_type::from_promise(*this)}; }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    std::suspend_always yield_value(T value) {
        current_value = std::move(value);
        return {};
    }
    void return_void() {}
    void unhandled_exception() { std::terminate(); }
};

generator <uint64_t> fibonacci(unsigned n) {
    uint64_t a = 0, b = 1;
    for (unsigned i = 0; i < n; ++i) {
        co_yield a;
        auto tmp = a;
        a = b;
        b = tmp + b;
    }
}

Usage:

int main() {
    for (auto v : fibonacci(10)) {
        std::cout << v << ' ';
    }
    // Output: 0 1 1 2 3 5 8 13 21 34
}

3. co_await and awaitable

co_await works with types that satisfy the Awaitable concept. The compiler transforms the co_await expression into calls to:

await_ready()   // true → skip suspension
await_suspend() // receives the coroutine handle; returns true if suspension needed
await_resume()   // value returned after resumption

A trivial awaitable that completes immediately:

struct immediate_awaitable {
    bool await_ready() noexcept { return true; }
    void await_suspend(std::coroutine_handle<>) noexcept {}
    void await_resume() noexcept {}
};

async void example() {
    co_await immediate_awaitable{}; // no suspension
}

For I/O, you typically wrap platform APIs (e.g., Boost.Asio, libuv) into awaitables that resume when data arrives.


4. Common Pitfalls

Issue Fix
Exception safety Ensure promise_type::unhandled_exception handles or rethrows.
Lifetime of awaitables Avoid returning references to local objects from await_resume.
State machine size Keep coroutine functions small; large state machines hurt stack size.
Blocking inside coroutines Never call blocking APIs directly; wrap them in awaitables that resume asynchronously.

5. Where to Use Coroutines

  • Lazy sequences: generator types that produce values on demand.
  • Async I/O: Wrapping sockets, files, or database queries.
  • Reactive pipelines: Compose streams of data with co_yield.
  • Coroutines as continuations: Offload work to a thread pool with custom awaitables.

6. TL;DR

C++ coroutines provide a powerful abstraction for asynchronous and lazy programming. By understanding the promise type, awaitable interface, and coroutine control flow, you can write cleaner, non‑blocking code that integrates seamlessly with existing C++ ecosystems. Start small—create a generator, then evolve to complex I/O workflows—and you’ll unlock the full potential of modern C++.

**Why Should I Prefer `std::optional` Over Pointers for Nullable Return Values?**

std::optional was introduced in C++17 to provide a safer, more expressive way of handling values that may or may not be present. While raw pointers (including nullptr) have long been a de facto idiom for nullable returns, std::optional offers several advantages that align with modern C++ best practices:

Feature Raw Pointer std::optional
Type safety The compiler cannot distinguish between a valid object and a null pointer without manual checks. The presence of a value is encoded in the type (`std::optional
`), forcing explicit handling.
Value semantics Returning a pointer can accidentally lead to shared ownership or unintended lifetime extension. The wrapped value is stored directly (or via a small buffer), guaranteeing copy/move semantics.
Memory overhead Pointers consume 8 bytes on 64‑bit systems and require a separate allocation if dynamic. Only an extra flag (bool or bitfield) is needed in addition to the value; no dynamic allocation.
RAII compliance Raw pointers need manual delete/free or smart‑pointer helpers. The optional owns its value; destruction is automatic.
Error handling Must rely on sentinel values or exception handling. Provides has_value(), value_or(), and operator*()/operator-> for natural access.

Practical Example

#include <optional>
#include <string>
#include <iostream>

std::optional<std::string> read_file(const std::string& path) {
    if (path == "missing.txt")
        return std::nullopt;           // No value
    return std::string("File content..."); // Value present
}

int main() {
    auto content = read_file("missing.txt");
    if (!content) {
        std::cerr << "File not found.\n";
        return 1;
    }

    // Access the string safely
    std::cout << *content << '\n';
}

With raw pointers:

std::string* read_file(const std::string& path) {
    if (path == "missing.txt")
        return nullptr;
    return new std::string("File content...");
}

int main() {
    std::unique_ptr<std::string> content(read_file("missing.txt"));
    if (!content) {
        std::cerr << "File not found.\n";
        return 1;
    }
    std::cout << *content << '\n';
}

The pointer version requires manual memory management and uses unique_ptr to avoid leaks—an extra layer of complexity that std::optional removes.

When to Still Use Pointers

  • Polymorphic objects: If the return type is a base class pointer to a derived object, `std::unique_ptr
  • Legacy interfaces: When interfacing with APIs that already use pointers, std::optional may not be applicable without refactoring.

Bottom Line

std::optional is a first‑class citizen in C++17 and later, and for most functions that return a value that might be absent, it should be the default choice. It makes intent explicit, enforces safer code, and keeps the implementation free from manual memory management pitfalls.

C++20中的概念(Concepts)及其在模板编程中的实际应用

在 C++20 之前,模板是编译器级别的泛型工具,使用时几乎没有任何类型约束,导致错误信息难以理解,并且在编译时无法提前检测到不合法的模板实参。C++20 引入了 概念(Concepts),提供了一种更直观、可读性更好的方式来表达模板参数的约束。本文将系统介绍概念的语法、实现方式以及在实际项目中的应用场景,并演示如何将现有的模板代码迁移到使用概念的版本。

1. 什么是概念?

概念是对类型或表达式的一组约束(约束可以是类型成员、表达式可满足性、继承关系等)的抽象描述。它们可以在模板参数列表中声明,并通过 requires 子句或更简洁的语法直接写在模板参数后面。概念的主要作用有:

  • 可读性:在代码中直接表明参数所需满足的特性。
  • 错误信息:编译器在参数不满足概念时会给出更精确的错误提示。
  • 编译时优化:通过约束可以让编译器进行更充分的推断,避免不必要的实例化。

2. 基本语法

template<typename T>
concept Integral = std::is_integral_v <T>;

template<Integral T>
T add(T a, T b) { return a + b; }

上面示例中,Integral 是一个概念,要求模板参数 T 必须是整型。add 函数只能接受满足 Integral 的类型。

概念可以组合:

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

template<typename T>
concept SignedIncrementable = Incrementable <T> && std::signed_integral<T>;

3. requires 子句

除了使用概念标签外,也可以在模板前使用 requires 子句做约束:

template<typename T>
requires std::same_as<T, int>
int square(int x) { return x * x; }

这在不想给概念起名字时非常方便。

4. 现有代码迁移案例

假设我们有一个通用的 swap 函数:

template<typename T>
void swap(T& a, T& b) {
    T tmp = std::move(a);
    a = std::move(b);
    b = std::move(tmp);
}

这个实现可以对任何可移动的类型使用,但在某些场景下会产生不必要的移动构造/移动赋值。我们可以引入概念来限制:

template<typename T>
concept MoveAssignable = std::is_move_assignable_v <T>;

template<MoveAssignable T>
void swap(T& a, T& b) noexcept(noexcept(std::move(a))) {
    T tmp = std::move(a);
    a = std::move(b);
    b = std::move(tmp);
}

现在,swap 只会在 T 满足移动可赋值时才参与编译。这样既保持了泛型的灵活性,又避免了无意义的实例化。

5. 在标准库中的运用

C++20 标准库已经大量使用概念,例如 std::ranges::rangestd::invocable 等。下面演示如何用概念实现一个轻量级的 for_each

template<std::ranges::range R, std::invocable<std::ranges::range_value_t<R>&> F>
void for_each(R&& r, F&& f) {
    for (auto& elem : r) {
        std::invoke(std::forward <F>(f), elem);
    }
}

此实现会在编译时检查传入的容器 R 是否满足 std::ranges::range,以及函数 F 是否可以接受容器元素的引用。

6. 性能与编译时间

概念本身是编译期的静态检查,对运行时性能没有任何影响。虽然在某些极端情况下,概念的使用会导致编译时间略有增长(因为需要额外的约束检查),但这在现代编译器中已得到显著优化。相较之下,使用概念可以避免无用的模板实例化,从而减少最终二进制文件的大小。

7. 进一步阅读与资源

  • C++20 Concepts – 官方 C++ 标准草案中关于概念的章节。
  • “C++20 Concepts in Depth” – 详细的博客文章,适合从基础到高级的系统学习。
  • Boost ConceptTS – Boost 为 C++11/14/17 提供的概念实现,在 C++20 之前也可使用。

结语

概念为 C++ 模板编程提供了更严谨、更易维护的工具。无论是对第三方库的约束,还是对自身项目的类型安全,都能通过概念显著提升代码质量。建议在编写新的泛型代码时,优先考虑使用概念来定义接口,而不是传统的 SFINAE 或 enable_if 方案。随着编译器对概念的支持日益完善,未来的 C++ 代码将更加清晰、健壮。

C++20 Concepts: Enhancing Type Safety in Template Metaprogramming

在C++17之前,模板编程往往需要通过SFINAE、std::enable_if、以及各种static_assert来约束模板参数。虽然这些技巧强大,但代码可读性差、错误信息模糊,而且往往在编译期间产生大量无意义的错误。C++20引入的概念(Concepts)则彻底改变了这一局面。

1. 什么是概念?

概念是对模板参数类型约束的声明,它们描述了一组“期望的”特性或行为。与传统的SFINAE相比,概念可以:

  • 提高可读性:概念名称本身表达了意图,如 std::integralstd::sortable
  • 减少错误信息噪音:编译器会在概念不满足时给出更直观的错误信息。
  • 支持编译时可判定:概念可以在编译期间进行逻辑组合,避免运行时开销。

2. 基础语法

template<typename T>
concept Integral = std::is_integral_v <T> && requires(T a, T b) {
    { a + b } -> std::convertible_to <T>;
};

template<Integral T>
T add(T a, T b) { return a + b; }

这里 Integral 是一个概念,使用 requires 子句来约束 T 的行为。add 函数只接受满足 Integral 概念的类型。

3. 组合与继承

概念可以通过逻辑运算符 (&&, ||, !) 组合:

template<typename T>
concept SignedIntegral = Integral <T> && std::is_signed_v<T>;

template<SignedIntegral T>
T subtract(T a, T b) { return a - b; }

此外,概念也可以继承:

template<typename T>
concept Arithmetic = Integral <T> || FloatingPoint<T>;

template<Arithmetic T>
T multiply(T a, T b) { return a * b; }

4. 与标准库的结合

C++20 标准库已经提供了大量概念,例如:

  • std::equality_comparable
  • std::sortable
  • std::ranges::range

这些概念可以直接用于算法模板,提高代码的自文档化。

#include <algorithm>
#include <vector>
#include <concepts>

template<std::ranges::range R>
void bubble_sort(R&& r) {
    for (std::size_t i = 0; i < std::size(r); ++i)
        for (std::size_t j = 0; j + 1 < std::size(r) - i; ++j)
            if (std::ranges::begin(r)[j] > std::ranges::begin(r)[j + 1])
                std::swap(std::ranges::begin(r)[j], std::ranges::begin(r)[j + 1]);
}

5. 概念与类型擦除

概念可以配合类型擦除(type erasure)实现更灵活的接口:

template<std::movable T>
class Any {
public:
    template<class U>
    requires std::convertible_to<U, T>
    Any(U&& u) : ptr_(new Wrapper<std::decay_t<U>>(std::forward<U>(u))) {}
    // ...
private:
    struct Concept { virtual ~Concept() = default; };
    template<class U>
    struct Wrapper : Concept {
        U value;
        Wrapper(U&& v) : value(std::move(v)) {}
    };
    Concept* ptr_;
};

6. 实际项目中的收益

  1. 更快的编译错误定位
    当不满足概念时,编译器会直接指出哪个概念未满足,而不是深入到模板展开的每一层。

  2. 更少的SFINAE代码
    用概念替代 std::enable_if 可以让模板代码更简洁,减少“技巧”代码。

  3. 增强的 API 可读性
    API 设计者可以在参数列表中直接看到约束,调用者无需再查看文档即可理解。

7. 小结

C++20 的概念为模板编程带来了更高的类型安全、可读性和可维护性。它们通过在编译期间明确约束,避免了传统 SFINAE 产生的模糊错误。无论是构建标准库还是自定义库,熟练使用概念都是现代 C++ 开发者的必备技能。持续关注社区对概念的扩展和改进,将帮助你写出更安全、更简洁、更高效的模板代码。

**Leveraging Ranges and Views in C++20 for Cleaner Code**

C++20 introduces the Ranges library, which dramatically changes how we process sequences. Instead of writing loops, temporary containers, and manual copies, ranges let us express intent directly on the data. Views are lazy transformations that avoid allocating intermediate storage, while ranges bring algorithms that operate over arbitrary ranges.


1. Why Ranges?

Before C++20, most algorithms required iterators or full containers:

std::vector <int> v = {1,2,3,4,5};
std::vector <int> result;
std::copy_if(v.begin(), v.end(), std::back_inserter(result),
             [](int x){ return x % 2 == 0; });

This code copies matching elements into a new vector. With ranges, you can express the same idea as a pipeline:

auto evens = v | std::views::filter([](int x){ return x % 2 == 0; });
for (int n : evens) std::cout << n << ' ';

No temporary vector is created; the filter view lazily evaluates each element on demand.


2. The Core Components

Component Purpose
std::ranges::views Namespace containing lazy, composable transformations
std::ranges::action In-place transformations (e.g., std::ranges::sort)
std::ranges::begin/end Generic accessors that work with any range
std::ranges::size Size query that works on containers and views

3. Building a Pipeline

Below is a complete example that:

  1. Reads a file line by line.
  2. Filters lines containing the word “error”.
  3. Transforms those lines to uppercase.
  4. Counts how many error lines were found.
#include <iostream>
#include <fstream>
#include <string>
#include <ranges>
#include <algorithm>
#include <cctype>

int main() {
    std::ifstream file("log.txt");
    if (!file) return 1;

    auto lines = std::views::istream(file);

    auto error_lines = lines
        | std::views::filter([](const std::string& s){
            return s.find("error") != std::string::npos;
          })
        | std::views::transform([](std::string s){
            std::transform(s.begin(), s.end(), s.begin(),
                           [](unsigned char c){ return std::toupper(c); });
            return s;
          });

    for (auto& line : error_lines) {
        std::cout << line << '\n';
    }

    std::cout << "Total errors: " << std::ranges::distance(error_lines) << '\n';
}

Key takeaways:

  • istream View: std::views::istream lazily reads lines as needed.
  • Filter View: Eliminates non-error lines without storing them.
  • Transform View: Performs the uppercase conversion on the fly.
  • Distance: Works on the view to count elements without iterating twice.

4. Common View Types

View Description
std::views::filter Selects elements that satisfy a predicate
std::views::transform Applies a unary function to each element
std::views::take / take_while Limits the number of elements
std::views::drop / drop_while Skips elements
std::views::reverse Provides a reversed view
std::views::zip (from third‑party) Pairs elements from multiple ranges

5. Performance Notes

  • Lazy evaluation: Views compute elements only when iterated.
  • No temporary containers: Reduces memory overhead and improves cache locality.
  • Small function objects: Prefer constexpr lambdas to enable compile‑time optimization.

6. Advanced Usage

a. Custom View

You can write a view that generates Fibonacci numbers:

struct fibonacci_view : std::ranges::view_interface <fibonacci_view> {
    struct iterator {
        std::size_t cur = 0, next = 1;
        std::size_t operator*() const { return cur; }
        iterator& operator++() {
            std::size_t tmp = cur + next;
            cur = next; next = tmp;
            return *this;
        }
        bool operator!=(const iterator&) const { return true; } // infinite
    };

    iterator begin() const { return {}; }
    std::ranges::sentinel_t <iterator> end() const { return {}; }
};

b. Action vs View

If you need to modify a container in place:

std::vector <int> vec = {3,1,4,1,5};
vec | std::ranges::sort;
vec | std::ranges::reverse;

The std::ranges::action pipeline mutates vec directly.


7. Bottom Line

Ranges and views transform C++ code from a “loop‑heavy” language to a “pipeline‑oriented” one. By composing small, lazy transformations, you write more declarative, readable, and efficient code. The C++20 standard encourages this style; modern IDEs and compilers optimize these pipelines aggressively, making them a powerful tool for every C++ developer.

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

在多线程环境下,单例模式的实现需要确保只有一个实例被创建,并且在并发访问时不会出现竞争条件。下面以 C++17 为例,演示几种常见的线程安全实现方式,并说明各自的优缺点。

1. C++11 的 std::call_once

最直接、最推荐的方式是使用 std::call_oncestd::once_flag。它们是标准库提供的原子操作,天然线程安全,且在第一次调用时才会初始化。

#include <iostream>
#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag_, []() { instancePtr_ = new Singleton(); });
        return *instancePtr_;
    }

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

    void sayHello() const { std::cout << "Hello from Singleton\n"; }

private:
    Singleton() { std::cout << "Singleton constructed\n"; }
    ~Singleton() { std::cout << "Singleton destroyed\n"; }

    static Singleton* instancePtr_;
    static std::once_flag initFlag_;
};

Singleton* Singleton::instancePtr_ = nullptr;
std::once_flag Singleton::initFlag_;

优点

  • 简洁:代码量少,易于维护。
  • 高效:只有第一次调用会触发一次性初始化,后续调用几乎不消耗资源。
  • 标准:属于 C++ 标准库,保证在所有标准实现上的行为一致。

缺点

  • 需要手动管理单例的销毁,如果不使用 std::unique_ptrstd::shared_ptr,程序退出时会留下悬挂的静态对象,可能导致析构顺序问题。

2. 局部静态变量(Meyers Singleton)

C++11 之后,局部静态变量的初始化已被保证为线程安全。只需在函数内部声明静态对象即可。

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;  // 线程安全初始化
        return instance;
    }

    // ...
};

优点

  • 最简洁:几乎不需要任何额外代码。
  • 自动销毁:静态对象在程序结束时按正确顺序析构。

缺点

  • 延迟初始化:如果 instance() 从未被调用,单例永不构造。
  • 难以控制:无法显式控制实例的生命周期,例如提前销毁或重建。

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

传统的双重检查锁方案在 C++11 之前不安全,但使用 std::atomic 后可以工作。示例代码:

#include <atomic>
#include <mutex>

class Singleton {
public:
    static Singleton* instance() {
        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;
    }

    // ...
private:
    Singleton() = default;
    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

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

优点

  • 延迟初始化:首次访问时才构造。
  • 性能:在已初始化后,无需加锁即可获取实例。

缺点

  • 实现繁琐:需要仔细处理内存序和锁,错误可能导致难以调试的并发 bug。
  • 不推荐std::call_once 更加直观且已被标准化。

4. 线程局部单例(Thread-Local Singleton)

有时你需要为每个线程维护一个独立的单例实例。可以使用 thread_local 关键字。

class ThreadSingleton {
public:
    static ThreadSingleton& instance() {
        thread_local ThreadSingleton instance;
        return instance;
    }

    // ...
};

适用场景

  • 需要线程级别的隔离,例如线程安全的日志记录器。
  • 不想共享状态导致竞争的场景。

小结

  • 推荐:使用 std::call_once + std::once_flag 或者局部静态变量(Meyers)实现单例。
  • 不推荐:双重检查锁需要细致的原子操作,容易出错。
  • 特殊需求:如果每个线程需要独立实例,考虑 thread_local

下面给出一个完整示例,演示在多线程环境下安全获取单例,并记录线程 ID:

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <chrono>

class Logger {
public:
    static Logger& instance() {
        static Logger instance;  // 线程安全初始化
        return instance;
    }

    void log(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << "[Thread " << std::this_thread::get_id() << "] " << msg << '\n';
    }

private:
    Logger() = default;
    ~Logger() = default;
    std::mutex mutex_;
};

void worker(int id) {
    for (int i = 0; i < 5; ++i) {
        Logger::instance().log("Worker " + std::to_string(id) + " iteration " + std::to_string(i));
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

int main() {
    const int numThreads = 4;
    std::vector<std::thread> threads;
    for (int i = 0; i < numThreads; ++i) {
        threads.emplace_back(worker, i);
    }
    for (auto& t : threads) t.join();
    return 0;
}

运行结果显示,所有线程共享同一个 Logger 实例,输出顺序不确定,但线程 ID 一致,证明单例在多线程环境下的正确性。

如何在 C++20 中使用 consteval 函数实现编译期整数平方?

在 C++20 之前,常见的做法是利用 constexpr 关键字配合模板递归或 std::integral_constant 来在编译期计算整数平方。随着 C++20 引入 consteval,我们可以更直观地声明必须在编译期求值的函数,从而在编译期实现整数平方并避免不必要的运行时开销。下面将详细介绍 consteval 的使用方法,并给出完整可运行的示例。

1. consteval 的基本语义

consteval int square(int x) {
    return x * x;
}
  • 必须在编译期求值:如果编译器无法在编译期求值该函数(例如传入的参数是非常量表达式),编译会报错。
  • 不能返回引用consteval 函数的返回值必须是实值,不能是对对象的引用。
  • 可用于递归:若需要更复杂的编译期计算,consteval 与模板或递归函数可配合使用。

2. 通过 consteval 实现整数平方

#include <iostream>
#include <type_traits>

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

调用方式:

int main() {
    constexpr int val = square(5);          // 编译期求值
    std::cout << "5 squared is " << val << '\n';

    // 下面这行会触发编译错误,因为  n  不是常量表达式
    // int n = 3;
    // std::cout << square(n) << '\n';
}

3. 用 consteval 生成编译期常量表

有时我们想一次性生成一个整数平方表。可以使用 consteval 函数返回 std::array

#include <array>
#include <utility>

template<std::size_t N>
consteval std::array<int, N> generate_square_table() {
    std::array<int, N> arr{};
    for (std::size_t i = 0; i < N; ++i) {
        arr[i] = static_cast <int>(i) * static_cast<int>(i);
    }
    return arr;
}

int main() {
    constexpr auto table = generate_square_table <10>();
    for (auto v : table) {
        std::cout << v << ' ';
    }
    std::cout << '\n';
}

此程序在编译期完成数组生成,运行时只做一次输出。

4. 与模板元编程的互补

如果你想在更通用的环境下使用 consteval,可以结合模板特化:

template<int X>
struct Square {
    static constexpr int value = X * X;
};

consteval int square_via_template(int x) {
    return Square <x>::value;   // 编译期展开
}

这里 Square 是编译期计算的结构体,square_via_template 使用 consteval 来强制在编译期调用。

5. 何时使用 consteval

  • 确定性编译期计算:当你想让编译器强制在编译期完成计算,避免运行时产生错误时。
  • 类型安全consteval 可与 constexpr 结合使用,保证类型安全和性能。
  • API 设计:在库设计中,可以通过 consteval 标记必须在编译期调用的函数,以便更好地文档化行为。

6. 完整示例

#include <iostream>
#include <array>
#include <type_traits>

// 1. 简单的 consteval 函数
consteval int square(int x) {
    return x * x;
}

// 2. 生成平方表
template<std::size_t N>
consteval std::array<int, N> generate_square_table() {
    std::array<int, N> arr{};
    for (std::size_t i = 0; i < N; ++i) {
        arr[i] = static_cast <int>(i) * static_cast<int>(i);
    }
    return arr;
}

int main() {
    // 使用 consteval 计算单个值
    constexpr int sq5 = square(5);
    std::cout << "5 squared = " << sq5 << '\n';

    // 生成 0~9 的平方表
    constexpr auto table = generate_square_table <10>();
    std::cout << "Square table: ";
    for (int v : table) std::cout << v << ' ';
    std::cout << '\n';

    return 0;
}

编译运行后输出:

5 squared = 25
Square table: 0 1 4 9 16 25 36 49 64 81

7. 小结

  • consteval 强制函数在编译期求值,适用于不允许运行时计算的场景。
  • constexpr、模板递归等技术配合,可实现更复杂的编译期算法。
  • 通过 consteval 能提升代码的安全性和性能,并使得编译器能够在编译阶段捕获错误。

在现代 C++ 开发中,合理利用 consteval 能让你的代码在保持高性能的同时,也更易于维护和错误检查。祝你编程愉快!

Exploring the Power of C++20 Coroutines: A Practical Guide

C++20 introduced coroutines, a language feature that lets you write asynchronous, lazy, and streaming code in a natural, synchronous-looking style. Coroutines enable the definition of functions that can suspend execution and resume later, making it easier to build pipelines, generators, and asynchronous APIs without the boilerplate of callbacks or state machines.


1. What Is a Coroutine?

A coroutine is a function that can pause (co_await, co_yield, co_return) and later resume from the same point. Under the hood, the compiler transforms the coroutine into a state machine that preserves local variables across suspensions.

  • co_await – Suspends until an awaited awaitable completes.
  • co_yield – Produces a value and suspends, similar to yield in Python.
  • co_return – Returns a value and finishes the coroutine.

Coroutines are typically used in three patterns:

  1. Generator – Produces a sequence of values lazily.
  2. Task/Async – Represents an asynchronous operation.
  3. Pipeline – Chains multiple suspensions for data processing.

2. Generators: Lazy Sequences

A generator returns a value each time it is resumed. The result type must be a custom awaitable or generator type. The standard library does not yet provide a full generator type, but many libraries (e.g., cppcoro, Boost.Coroutine2) offer implementations. Below is a minimal custom generator:

#include <iostream>
#include <coroutine>
#include <exception>

template<typename T>
struct Generator {
    struct promise_type {
        T current_value;
        std::exception_ptr exception;

        Generator get_return_object() {
            return Generator{
                .handle = std::coroutine_handle <promise_type>::from_promise(*this)
            };
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        void return_void() {}
        void unhandled_exception() { exception = std::current_exception(); }
    };

    std::coroutine_handle <promise_type> handle;

    explicit Generator(std::coroutine_handle <promise_type> h) : handle(h) {}
    ~Generator() { if (handle) handle.destroy(); }

    struct Iterator {
        std::coroutine_handle <promise_type> coro;
        bool done = false;

        Iterator(std::coroutine_handle <promise_type> c, bool d) : coro(c), done(d) {}
        Iterator& operator++() {
            coro.resume();
            if (coro.done() || coro.promise().exception)
                done = true;
            return *this;
        }
        T operator*() const { return coro.promise().current_value; }
        bool operator!=(const Iterator& other) const { return done != other.done; }
    };

    Iterator begin() { return Iterator{handle, !handle || handle.done()}; }
    Iterator end() { return Iterator{handle, true}; }
};

Generator <int> natural_numbers(int start = 0) {
    int n = start;
    while (true) {
        co_yield n++;
    }
}

int main() {
    auto gen = natural_numbers(5);
    int count = 0;
    for (auto n : gen) {
        std::cout << n << ' ';
        if (++count == 10) break; // manual stop
    }
}

This generator lazily produces natural numbers starting from 5. The loop stops after ten numbers to avoid an infinite sequence.


3. Async Tasks: Non-Blocking Operations

C++20’s std::future and std::async provide basic asynchronous facilities, but coroutines allow a cleaner integration with event loops and I/O libraries.

A simple Task type that returns a value asynchronously:

#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>

template<typename T>
struct Task {
    struct promise_type {
        T value;
        std::exception_ptr exception;

        Task get_return_object() {
            return Task{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_value(T v) { value = std::move(v); }
        void unhandled_exception() { exception = std::current_exception(); }
    };

    std::coroutine_handle <promise_type> handle;

    Task(std::coroutine_handle <promise_type> h) : handle(h) {}
    ~Task() { if (handle) handle.destroy(); }

    T get() {
        handle.resume();
        if (handle.promise().exception) std::rethrow_exception(handle.promise().exception);
        return handle.promise().value;
    }
};

Task <int> async_add(int a, int b) {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    co_return a + b;
}

int main() {
    auto result = async_add(3, 4).get();
    std::cout << "Result: " << result << '\n';
}

Here async_add simulates a long-running operation using sleep_for. In real-world code, you would replace this with non-blocking I/O.


4. Pipelines: Composing Coroutines

Coroutines can be chained to build data pipelines. For example, read lines from a file, transform them, and write them elsewhere.

#include <fstream>
#include <string>
#include <iostream>
#include <coroutine>

template<typename T>
struct Stream {
    struct promise_type { ... }; // similar to Generator
};

Stream<std::string> read_lines(std::istream& in) {
    std::string line;
    while (std::getline(in, line)) {
        co_yield line;
    }
}

Stream<std::string> to_upper(std::string input) {
    for (auto& c : input) c = std::toupper(c);
    co_return std::move(input);
}

int main() {
    std::ifstream file("input.txt");
    for (auto line : read_lines(file)) {
        std::string upper = to_upper(line).get(); // convert to upper case
        std::cout << upper << '\n';
    }
}

By separating concerns, each coroutine handles a specific transformation, and the main loop stays clean.


5. Integrating with Libraries

Many modern libraries now support coroutines:

  • Boost.Coroutine2 – Provides a robust coroutine infrastructure.
  • cppcoro – Offers generator, task, sync_wait.
  • Asio – Asynchronous I/O with co_await in C++20.

Using these, you can avoid writing boilerplate state machines and handle errors elegantly with exceptions.


6. Tips and Best Practices

  1. Return Types – Prefer `generator ` for streams and `task` for async results.
  2. Error Handling – Store exceptions in the promise and rethrow on get().
  3. Resource Management – Ensure handles are destroyed; the coroutine handles its own stack.
  4. Performance – Avoid excessive co_yield in tight loops; the state machine overhead is minimal but non-zero.
  5. Testing – Treat coroutines as regular functions; you can step through with debuggers that support coroutines.

7. Conclusion

C++20 coroutines elevate asynchronous and lazy programming to a new level of expressiveness and simplicity. By leveraging generators, async tasks, and pipelines, developers can write clearer code that mimics synchronous styles while still benefiting from non-blocking execution. As the ecosystem matures, expect more libraries to embrace coroutines, making this feature a staple for modern C++ development.