文章目录
- 前言
- [概念 concept 与类型特征 type_trait](#概念 concept 与类型特征 type_trait)
- 一个模板线程池的写法
推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[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::forward、std::invoke、std::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++ 的线程池.
之后是模板线程池的类型定义。在其体内需要定义任务类型 TaskBase 和 ConcreteTask,使其拥有对外执行的接口 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