【C++20新特性】概念约束特性与 “模板线程池”,概念约束是为了 “把握未知对象”

文章目录

推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接

前言

概念与约束是 C++20 的五大新特性之一,其余四个是 constexpr 元编程、协程 coroutine、范围 range 和模块 module。

C++ 20 的概念与约束特性是专门服务于模板元编程的,它让模板类型、模板函数的使用变得更加方便,他只允许符合概念约束的类型传入模板。相比于不加限制修饰的模板,对于加了概念描述的模板来说,在类型还未套入的模板的时候,模板就还可以根据概念对未传入的类型进行操作、调用。

一个思考问题,请问模板线程池该怎么写(任务函数作为模板)?

  • 1、函数类型当然可以被套入模板了
    2、但没有概念约束的未传入类型太过抽象了,我无法把握他究竟是函数类型还是其他类型(比如字符串)
    3、如何解决这个无从下手的窘迫感?

概念 concept 与类型特征 type_trait

重要的事情放在最前面:Concepts 和 Type Traits 本质上都是编译期布尔常量

Type Traits(类型特征)是 C++11 引入的编译时类型反射机制,属于模板元编程的核心组件。它提供了一组用于在编译时查询、检查和转换类型信息的模板类和函数。(不止有这一个的)

Trait 名称 头文件 描述 值类型 用法示例
is_invocable <type_traits> 是否可用 Args 调用 F bool is_invocable_v<F, Args...>

概念(Concept) 是 C++20 引入的命名约束集合(named set of constraints),用于在编译期对模板参数进行约束规范(constraint specification),实现模板参数的语义要求(semantic requirements)的显式表达。

概念名称 所在头文件 描述 用法示例
same_as <concepts> 两个类型完全相同 requires same_as<T, int>
derived_from <concepts> 类型继承自另一个 derived_from<Derived, Base>
convertible_to <concepts> 可隐式转换为目标类型 convertible_to<int, double>
invocable <functional> 可调用 invocable<F, Args...>

我的理解:Concepts 和 Type Traits 本质上都是编译期布尔常量,都是在讨论某一个具体类型 是否 拥有某个性质 。另外,Concept 只是对 Type Traits 的包装,组合起来。

cpp 复制代码
// Type Trait(C++11起)
template<typename T>
struct is_function : std::false_type {};

template<typename R, typename... Args>
struct is_function<R(Args...)> : std::true_type {};

template<typename T>
inline constexpr bool is_function_v = is_function<T>::value;  // 编译期布尔常量

=======================================================================

// Concept(C++20起)
template<typename T>
concept FunctionSignature = std::is_function_v<T>;  // 本质上也是编译期布尔常量

类型检查通过,对我们有什么好处呢?答案:避免错误的发生,避免我们在调用某些函数的时候发生错误 。它的意思是某些函数模板不可以随便调用,要通过概念、类型检查在编译期检查清楚。准备完成之后,我们便可以使用万能转发 std::forwardstd::invokestd::apply 等函数。

cpp 复制代码
// 模板元编程(编译时) ←→ 实用函数(运行时/泛型)

// 模板元:定义规则和约束(警察)
template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::same_as<T>;  // 编译时检查
};

// 实用函数:执行具体操作(执行者)
template<Addable T>
T add_checked(T a, T b) {
    return std::invoke([](auto&& x, auto&& y) {
        return std::forward<decltype(x)>(x) + std::forward<decltype(y)>(y);
    }, a, b);
}

这其实一下子就戳到了前面讲到的痛点,"模板代码跟普通代码一样直观可操作"。

模板代码直观操作 = 编译器类型检查 + 万能调用函数 模板代码直观操作=编译器类型检查+万能调用函数 模板代码直观操作=编译器类型检查+万能调用函数

先进行类型检查,之后再进行待套入模板的函数的万能调用。这样就是可以让模板代码,跟普通代码一样可操作的秘密。

另外,这三个标准库函数来自这三个头文件。首先是元组

cpp 复制代码
#include <tuple>

// 主要包含:
std::apply          // C++17,应用元组到函数
std::tuple          // 元组容器
std::make_tuple     // 创建元组
std::tie            // 解包元组到引用
std::tuple_cat      // 连接元组
std::get            // 访问元组元素
std::tuple_size     // 元组大小
std::tuple_element  // 元组元素类型

之后是完美转发 forward

cpp 复制代码
#include <utility>

// 主要包含:
std::forward        // 完美转发
std::move           // 移动语义
std::swap           // 交换
std::pair           // 键值对
std::integer_sequence // 整数序列
std::make_pair      // 创建pair
std::exchange       // 交换并返回旧值
std::as_const       // 添加const
// 各种比较操作符:<=>, ==, !=, <, > 等

最后是,万能调用 invoke

cpp 复制代码
#include <functional>

// 主要包含:
std::invoke        // C++17,通用调用
std::function      // 函数包装器
std::bind          // 参数绑定
std::ref, std::cref // 引用包装器
std::invoke_result // 调用结果类型
std::is_invocable  // 类型特性检查
// 各种函数对象:plus, minus, multiplies 等

最后是这三个函数的调用方法

cpp 复制代码
auto result = std::apply([this](auto&&... args) {
   return std::invoke(func_, std::forward<decltype(args)>(args)...);
}, args_);

一个模板线程池的写法

首先得要定义概念,我们这个线程池要处理什么类型的任务,即 FunctionSignature 这个概念;函数的返回值、参数可调用检查,即编译器结构体 FunctionTraits

cpp 复制代码
#pragma once

#include <thread>
#include <functional>
#include <vector>
#include <optional>
#include <tuple>
#include <concepts>
#include <type_traits>
#include <future>
#include <atomic>
#include <utility>
#include <stdexcept>

#include "BlockingQueue.hpp"

// ==================== 核心模板:固定函数类型的线程池 ====================

// 1. 定义函数签名 Concept
template<typename Signature>
concept FunctionSignature = std::is_function_v<Signature>;

// 2. 从函数类型提取返回类型和参数类型
template<FunctionSignature Sig>
struct FunctionTraits;

template<typename R, typename... Args>
struct FunctionTraits<R(Args...)> {
    using ReturnType = R;
    using ArgsTuple = std::tuple<std::decay_t<Args>...>;
    static constexpr size_t Arity = sizeof...(Args);
    
    // 检查是否可调用
    template<typename F>
    static constexpr bool IsInvocable = std::invocable<F, Args...>;
    
    // 检查返回类型是否匹配
    template<typename F>
    static constexpr bool ReturnTypeMatches = 
        std::is_same_v<std::invoke_result_t<F, Args...>, R>;
};

拥塞队列的代码 "BlockingQueue.hpp" 可以参考我这篇文章 【基础组件】手撕 C++ 的线程池.

之后是模板线程池的类型定义。在其体内需要定义任务类型 TaskBaseConcreteTask,使其拥有对外执行的接口 execute 和获取异步返回值的接口 get_future。线程池本身还需要检查任务本身是否合规,与线程池本身所处理的任务是否契合,契合才可以向线程池提交任务。

另外,我在 【C++ 面向对象编程】补档:线程池和 MySQL 连接池的设计模式分析 里面分析了线程池的本质是观察者模式。

cpp 复制代码
template<FunctionSignature Sig>
class ThreadPool {
private:
    using Traits = FunctionTraits<Sig>;
    using ReturnType = typename Traits::ReturnType;
    using ArgsTuple = typename Traits::ArgsTuple;
    
    // 检查任务是否匹配的辅助常量
    template<typename F>
    static constexpr bool IsStrictTask = 
        Traits::template IsInvocable<F> && 
        Traits::template ReturnTypeMatches<F>;
    
    // 任务基类
    class TaskBase {
    public:
        virtual ~TaskBase() = default;
        virtual void execute() = 0;
    };
    
    // 具体任务类
    template<typename F>
    class ConcreteTask : public TaskBase {
    private:
        F func_;
        ArgsTuple args_;
        std::optional<std::promise<ReturnType>> promise_;
        
    public:
        // 存储函数对象的值副本
        ConcreteTask(F func, ArgsTuple&& args, bool has_return_value)
            : func_(std::move(func))
            , args_(std::move(args)) {
            if (has_return_value) {
                promise_.emplace();
            }
        }

        void execute() override {
            try {
                if constexpr (std::is_void_v<ReturnType>) {
                    // 无返回值
                    // std::apply 的主要作用是将元组(tuple)或类似容器的元素展开,作为函数的参数。
                    // std::invoke 是一个通用的函数调用机制,可以调用任何可调用对象:函数指针、成员函数、成员变量、lambda、函数对象等。
                    std::apply([this](auto&&... args) {
                        std::invoke(func_, std::forward<decltype(args)>(args)...);
                    }, args_);
                } else {
                    // 有返回值
                    auto result = std::apply([this](auto&&... args) {
                        return std::invoke(func_, std::forward<decltype(args)>(args)...);
                    }, args_);
                    
                    // 传递回 future
                    if (promise_.has_value()) {
                        promise_->set_value(std::move(result));
                    }
                }
            } catch (...) {
                if (promise_.has_value()) {
                    promise_->set_exception(std::current_exception());
                }
            }
        }
        
        template<typename R = ReturnType>
        requires (!std::is_void_v<R>)
        std::future<R> get_future() {
            if (!promise_.has_value()) {
                throw std::logic_error("无返回值的任务不能获取 future");
            }
            return promise_->get_future();
        }
    };
public:
    // 初始化线程池
    explicit ThreadPool(int threads_num) {
        task_queue_ = std::make_unique<BlockingQueuePro<std::unique_ptr<TaskBase>>>();     //  这些任务
        for (int i = 0; i < threads_num; ++i) {
            workers_.emplace_back([this] {Worker();});
        }
    }

    // 停止线程池
    ~ThreadPool() {
        task_queue_->Cancel();
        for(auto &worker : workers_) {
            if (worker.joinable())
                //  joinable() 是 状态查询函数,用来判断一个 std::thread 对象当前是否代表一条"活着"的线程。
                worker.join();
        }
    }
    //  一句话:只要对象"生命周期结束",析构函数就会被自动调用。
    //  作用域结束、delete、容器销毁、程序结束、异常展开、成员/基类收尾------只要对象"该死"了,析构立刻执行。

    // 发布任务到线程池
    template<typename F, typename... Args>
    requires IsStrictTask<F>
    void submit(F&& func, Args&&... args) {
        static_assert(std::is_void_v<ReturnType>, 
            "此方法只适用于无返回值的任务。请使用 submit_with_future 获取返回值。");
        
        {   
            // 使用值捕获 Lambda
            auto captured_func = [func = std::forward<F>(func)](auto&&... fwd_args) 
                -> decltype(auto) {
                return std::invoke(func, std::forward<decltype(fwd_args)>(fwd_args)...);
            };
            
            task_queue_->Push(std::make_unique<ConcreteTask<decltype(captured_func)>>(
                std::move(captured_func),
                std::make_tuple(std::forward<Args>(args)...),
                false  // 无返回值
            ));
        }
    }

    template<typename F, typename... Args>
    requires IsStrictTask<F>
    std::future<ReturnType> submit_with_future(F&& func, Args&&... args) {
        static_assert(!std::is_void_v<ReturnType>, 
            "此方法只适用于有返回值的任务。请使用 submit 提交无返回值任务。");
        
        // 使用值捕获 Lambda 来安全存储
        auto captured_func = [func = std::forward<F>(func)](auto&&... fwd_args) 
            -> decltype(auto) {
            return std::invoke(func, std::forward<decltype(fwd_args)>(fwd_args)...);
        };
        
        // 创建任务并获取 future
        auto task = std::make_unique<ConcreteTask<decltype(captured_func)>>(
            std::move(captured_func),
            std::make_tuple(std::forward<Args>(args)...),
            true  // 有返回值
        );
        
        std::future<ReturnType> fut = task->get_future();
        task_queue_->Push(std::move(task));
        
        return fut;
    }

private:
    void Worker() {
        while (true) {
            std::unique_ptr<TaskBase> task;
            if (!task_queue_->Pop(task)) {
                break;
            }
            task->execute();
        }
    }

    std::unique_ptr<BlockingQueuePro<std::unique_ptr<TaskBase>>> task_queue_;      //  这是在使用拥塞队列的模板类定义,并且启用了构造函数
    std::vector<std::thread> workers_;
};

测试的代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <thread>
#include <chrono>

#include "ThreadPool.hpp"	//源码

void Producer(ThreadPool<void()>& pool, int producer_id, int num_tasks) {
    for (int i = 0; i < num_tasks; ++i) {
        int task_id = i;
        pool.submit([producer_id, task_id]() {
            std::cout << "生产者 " << producer_id << " 任务 " << task_id 
                      << " 在线程 " << std::this_thread::get_id() << std::endl;
        });
    }
}

int main() {
    const int num_threads_in_pool = 2;
    const int num_producers = 2;
    const int tasks_per_producer = 15;

    ThreadPool<void()> pool(num_threads_in_pool);

    std::vector<std::thread> producers;
    for (int i = 0; i < num_producers; ++i) {
        producers.emplace_back(Producer, std::ref(pool), i, tasks_per_producer);
    }

    // 等待所有生产者完成
    for (auto& producer : producers) {
        if (producer.joinable()) {
            producer.join();
        }
    }

    // 等待所有任务完成
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 测试有返回值的任务
    ThreadPool<int(int, int)> pool2(2);
    
    auto future1 = pool2.submit_with_future([](int a, int b) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        return a + b;
    }, 10, 20);
    
    auto future2 = pool2.submit_with_future([](int a, int b) {
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
        return a * b;
    }, 5, 6);
    
    std::cout << "结果1: " << future1.get() << std::endl;
    std::cout << "结果2: " << future2.get() << std::endl;

    return 0;
}

运行效果

bash 复制代码
qiming@k8s-master1:~/share/mycpp_work/c++20-trait/co-sche-v2$ g++ -std=c++20 -o test main.cpp -g -lpthread
qiming@k8s-master1:~/share/mycpp_work/c++20-trait/co-sche-v2$ ./test 
生产者 0 任务 0 在线程 140490174445120
生产者 0 任务 1 在线程 140490174445120
生产者 0 任务 2 在线程 140490174445120
生产者 0 任务 3 在线程 140490174445120
生产者 0 任务 4 在线程 140490174445120
生产者 0 任务 5 在线程 140490174445120
生产者 0 任务 6 在线程 140490174445120
生产者 0 任务 7 在线程 140490174445120
生产者 0 任务 8 在线程 140490174445120
生产者 0 任务 9 在线程 140490174445120
生产者 0 任务 10 在线程 140490174445120
生产者 0 任务 11 在线程 140490174445120
生产者 0 任务 12 在线程 140490174445120
生产者 0 任务 13 在线程 140490174445120
生产者 0 任务 14 在线程 140490174445120
生产者 1 任务 0 在线程 140490174445120
生产者 1 任务 1 在线程 140490174445120
生产者 1 任务 2 在线程 140490174445120
生产者 1 任务 3 在线程 140490174445120
生产者 1 任务 4 在线程 140490174445120
生产者 1 任务 5 在线程 140490174445120
生产者 1 任务 6 在线程 140490174445120
生产者 1 任务 7 在线程 140490174445120
生产者 1 任务 8 在线程 140490174445120
生产者 1 任务 9 在线程 140490174445120
生产者 1 任务 10 在线程 140490174445120
生产者 1 任务 11 在线程 140490174445120
生产者 1 任务 12 在线程 140490174445120
生产者 1 任务 13 在线程 140490174445120
生产者 1 任务 14 在线程 140490174445120
结果1: 30
结果2: 30
相关推荐
老蒋每日coding2 小时前
LangGraph:从入门到Multi-Agent超级智能体系统进阶开发
开发语言·python
你好!蒋韦杰-(烟雨平生)2 小时前
OpenGL
c++·数学·游戏·3d
郁闷的网纹蟒2 小时前
虚幻5---第12部分---蒙太奇
开发语言·c++·ue5·游戏引擎·虚幻
小旭95272 小时前
Java 反射详解
java·开发语言·jvm·面试·intellij-idea
雨季6662 小时前
Flutter 三端应用实战:OpenHarmony “极简文本行数统计器”
开发语言·前端·flutter·ui·交互
m0_748233173 小时前
PHP版本演进:从7.x到8.x全解析
java·开发语言·php
雨季6663 小时前
Flutter 三端应用实战:OpenHarmony 简易“动态字体大小调节器”交互模式深度解析
开发语言·flutter·ui·交互·dart
zhengfei6113 小时前
精选的优秀法证分析工具和资源列表
开发语言·php
当战神遇到编程3 小时前
图书管理系统
java·开发语言·单例模式