목차 제목
1. 소개
프로그래밍의 세계에서는 핵심 개념을 이해하고 숙달하는 것이 중요합니다. Bjarne Stroustrup이 "C++ 프로그래밍 언어"에서 말했듯이: "C++는 하드웨어에 대한 강력한 제어를 제공하는 직접적이고 효율적인 언어입니다." 이 기사에서는 일반적으로 사용되는 강력한 도구인 C ++, , 및 Lambda에 대해 자세히 설명 packaged_task
합니다 invoke_result_t
. 프로그래밍에서.bind
result_of
지식의 갈림길에 설 때마다 우리는 선택의 기로에 서게 됩니다. 각 방법, 기술 또는 도구에는 고유한 장점과 적용 가능한 시나리오가 있습니다. 우리가 선택하는 방법은 기능뿐만 아니라 문제에 대한 우리의 사고방식과 이해에 기초합니다. 이러한 C++ 기능을 탐색하면서 기술적인 관점뿐만 아니라 심리적인 관점에서도 특정 기능이 더 인기 있는 이유와 해당 기능의 디자인 철학이 그러한 방식인 이유를 이해해 보겠습니다.
1.1 C++의 급속한 발전
C++는 진화하는 언어입니다. C++는 처음부터 계속 발전하여 개발자에게 더 많은 도구와 더 높은 효율성을 제공했습니다. 그러나 도구와 기술의 증가로 인해 복잡성이 더욱 커졌습니다. 오래된 지혜에 따르면 "단순함은 단순화하는 것이 아니라 혼란스러운 복잡성 속에서 균형을 찾는 것입니다."
이전 버전의 C++에서는 이제 구식이거나 우아하지 않게 보일 수 있는 도구와 기술을 사용했습니다. 그러나 각 세대의 장인들이 자신이 원하는 도구를 사용하여 창작한 것처럼, 우리도 이러한 초기 방법이 오늘날 발전의 토대를 마련했기 때문에 존중해야 합니다.
1.2 이 글의 주제와 중요성
이 문서에서 설명하는 5가지 C++ 기능은 최신 C++ 개발의 핵심 개념입니다. 강력한 기능을 제공할 뿐만 아니라 더 중요한 것은 C++의 철학과 디자인 원칙을 표현한다는 것입니다.
예를 들어, 람다 표현식을 사용하면 추가 함수나 클래스를 정의할 필요 없이 간결하고 직관적인 방식으로 복잡한 작업을 표현할 수 있습니다. 비동기 프로그래밍과 함수형 프로그래밍을 위한 강력한 도구를 제공 합니다 std::bind
.std::packaged_task
이러한 기능을 자세히 알아보기 전에 해당 기능의 기원, 디자인 목표 및 실제 프로그래밍에서 사용하는 방법을 이해해야 합니다. 이런 방식으로 우리는 이러한 기능을 더 잘 이해할 수 있을 뿐만 아니라 C++를 보다 효과적으로 사용하는 방법도 배울 수 있습니다.
2. 이해하다std::packaged_task
std::packaged_task
호출 가능한 객체(함수, 람다, 멤버 함수 포인터 등)를 래핑하고 협력하여 비동기 작업을 수행하고 결과를 검색할 수 있는 C++11에 도입된 강력한 도구입니다 std::future
.
2.1 정의 및 주요 목적
std::packaged_task
本质上是一个包装器,它将任务与一个 std::future
对象关联在一起。当任务完成执行后,其结果(或异常)会存储在与之关联的 std::future
对象中。这意味着我们可以在任务完成后的任何时刻,从任何线程获取结果。
这种设计与人类的习惯性思维模式相契合。当我们向他人委托一个任务时,我们常常会想知道任务何时完成,以及最终的结果是什么。正如 Confucius 在《论语》中所说:“三人行,必有我师。” 我们可以从任何情境中学到知识,就像我们可以从任何线程中获取 std::packaged_task
的结果一样。
2.2 如何使用它与 std::future
配合执行异步任务
让我们通过一个简单的示例来展示这一点。
#include <iostream>
#include <future>
#include <thread>
int sum(int a, int b) {
return a + b;
}
int main() {
// 将函数包装到packaged_task中
std::packaged_task<int(int, int)> task(sum);
// 获取与packaged_task关联的future
std::future<int> result = task.get_future();
// 在另一个线程上执行任务
std::thread(std::move(task), 5, 3).detach();
// 在主线程上获取结果
std::cout << "Sum: " << result.get() << std::endl; // 输出 "Sum: 8"
return 0;
}
在上述代码中,我们定义了一个简单的函数 sum
,然后创建了一个 std::packaged_task
来包装这个函数。我们还创建了一个与这个任务关联的 std::future
对象,以便稍后检索结果。
2.3 实际示例
考虑一个复杂的场景,例如计算一个大数组的和。假设我们想把这个大数组分成小块,然后在多个线程上并行计算每个小块的和。最后,我们将所有这些小块的和加在一起,得到整个数组的和。
这种情况下,std::packaged_task
和 std::future
就非常有用了。我们可以为每个小块创建一个 std::packaged_task
,然后在不同的线程上执行它们。当所有这些任务都完成后,我们可以简单地从每个 std::future
对象中获取结果,并将它们加在一起。
正如 Bjarne Stroustrup 在《The C++ Programming Language》中所说:“C++ 的主要目的是使抽象成为现实。” 在这种情境下,std::packaged_task
和 std::future
允许我们将多线程编程的复杂性抽象出来,使其变得更加简单和直观。
此外,如果我们深入到 GCC 的源码中,可以看到 std::packaged_task
是如何实现的。在 libstdc++
的实现中,std::packaged_task
是定义在 future
头文件中的。它的主要功能是通过 _M_invoke
方法来执行存储的任务。这一切都是在 bits/future.h
文件中实现的。
3. 从 std::result_of
到 std::invoke_result_t
在早期的 C++ 标准中,std::result_of
是一个非常有用的工具,它可以帮助我们得知一个函数调用的返回类型。但是,随着时间的推移,我们发现它有一些不足之处,尤其是在新标准的上下文中。因此,C++17 引入了一个新的、更强大的工具:std::invoke_result_t
。
3.1 为什么需要这些工具?
在模板编程中,我们经常需要知道某个函数或可调用对象的返回类型。这些信息可以帮助我们为函数的输出创建适当的存储、做类型检查或决定如何进一步处理这些输出。
例如,当你有一个函数模板,它的返回类型取决于它的参数,或者当你有一个返回类型是 lambda 的函数。在这些情况下,你不能简单地查看函数的签名来确定它的返回类型,因为这个类型是动态的,取决于实际传递给函数的参数。
这正是 std::result_of
和 std::invoke_result_t
发挥作用的地方。
3.2 std::result_of
的工作方式
std::result_of
是一个模板类,它接受一个函数类型 F(Args...)
作为参数。其中,F
是可调用对象的类型,Args...
是一系列参数类型。这个模板类有一个名为 ::type
的嵌套类型,它表示调用该函数时的返回类型。
例如:
double foo(int, float);
std::result_of<decltype(foo)(int, float)>::type // 这是 double 类型
但是,std::result_of
有一些局限性,需要使用者非常小心。首先,你必须确保你提供的函数类型是有效的,否则你会得到一个编译错误。其次,如果函数不接受任何参数,那么你必须为它提供 void
类型的参数。
这种语法可能会对初学者造成困惑,并导致一些难以诊断的编译错误。
3.3 介绍 std::invoke_result_t
为了解决上述问题,C++17 引入了 std::invoke_result_t
。它的工作原理类似于 std::result_of
,但提供了更清晰、更直观的语法。
与 std::result_of
不同,std::invoke_result_t
直接接受函数和参数类型作为模板参数,并返回相应的返回类型。
例如:
double foo(int, float);
using ReturnType = std::invoke_result_t<decltype(foo), int, float>; // 这是 double 类型
这种新的语法清晰明了,很少有出错的机会。
3.4 示例对比两者的使用
让我们通过一个简单的示例来比较这两种工具的使用。
首先,使用 std::result_of
:
template<typename Func, typename Arg1, typename Arg2>
void print_result(Func f, Arg1 a1, Arg2 a2) {
using ResultType = typename std::result_of<Func(Arg1, Arg2)>::type;
ResultType result = f(a1, a2);
std::cout << "Result: " << result << std::endl;
}
现在,使用 std::invoke_result_t
:
template<typename Func, typename Arg1, typename Arg2>
void print_result(Func f, Arg1 a1, Arg2 a2) {
using ResultType = std::invoke_result_t<Func, Arg1, Arg2>;
ResultType result = f(a1, a2);
std::cout << "Result: " << result << std::endl;
}
正如Bjarne Stroustrup在《The C++ Programming Language》
中所说:“选择正确的工具是至关重要的,它不仅可以简化你的工作,还可以提高你的工作效率。”
3.5 总结
std::result_of
和 std::invoke_result_t
都是为了解决同一个问题而设计的:确定给定函数和参数的返回类型。但随着 C++ 的发展,我们发现 std::result_of
的语法和用法可能会导致错误和困惑。因此,C++17 引入了 std::invoke_result_t
,它提供了更清晰、更直观的语法,并减少了出错的机会。
4. 探索 std::bind
和 Lambda 表达式
在现代 C++ 编程中,函数对象和可调用实体扮演着非常重要的角色。它们为编程带来了巨大的灵活性,尤其是在高阶函数、多线程和异步编程中。这一章,我们将深入探讨两个非常有用的工具:std::bind
和 Lambda 表达式。
4.1 std::bind
的定义和主要用途
std::bind
是一个强大的函数模板,它返回一个可调用对象来“绑定”一个或多个参数。简而言之,它的主要作用是将给定的参数与函数或可调用对象绑定在一起,以产生一个新的无参数或减少参数的函数。
例如,假设我们有一个函数:
int add(int a, int b) {
return a + b;
}
使用 std::bind
,我们可以创建一个新的无参数函数,该函数在被调用时总是返回 3 + 4
的结果:
auto bound_add = std::bind(add, 3, 4);
std::cout << bound_add(); // 输出 7
正如 Bjarne Stroustrup 在《The C++ Programming Language》中所说:“使用 std::bind
可以非常灵活地组合函数和参数。”
但是,随着 C++11 的发展,引入了一个新的、更简洁的方式来实现相同的功能:Lambda 表达式。
4.2 Lambda 表达式的简介和其强大之处
Lambda 表达式(或简称 Lambda)是一种匿名函数对象。它为 C++ 添加了闭包的功能,闭包是一个可调用的实体,可以访问其创建位置的局部变量。这为 C++ 带来了巨大的编程灵活性。
例如,上面的 add
函数也可以使用 Lambda 表达式重写为:
auto lambda_add = [](int a, int b) {
return a + b;
};
std::cout << lambda_add(3, 4); // 输出 7
而要达到 std::bind
的效果,我们可以这样做:
auto bound_lambda = []() {
return lambda_add(3, 4);
};
std::cout << bound_lambda(); // 输出 7
Lambda 表达式的主要优点在于其简洁性和直观性。正如某位心理学家所说:“简洁性和直观性是有效沟通的关键。” 当我们阅读代码时,Lambda 表达式往往更容易理解,因为它们直接在使用的地方定义,而不需要查找其他地方的函数定义。
4.3 如何用 Lambda 替代 std::bind
从上面的例子中,我们可以看到,Lambda 表达式为我们提供了一种非常简洁的方式来绑定函数和参数。但在更复杂的情境下,Lambda 表达式和 std::bind
之间有什么区别呢?
考虑下面的例子:
void print_sum(int a, int b, int c) {
std::cout << a + b + c << std::endl;
}
// 使用 std::bind
auto bound_print = std::bind(print_sum, 1, 2, std::placeholders::_1);
bound_print(3); // 输出 6
// 使用 Lambda 表达式
auto lambda_print = [](int c) {
print_sum(1, 2, c);
};
lambda_print(3); // 输出 6
在这个例子中,我们使用了 std::placeholders
,它是 std::bind
的一部分,允许我们在调用绑定的函数时传递参数。与之相对,Lambda 表达式提供了一个更直接的方式来定义参数。
在源码级别,Lambda 表达式和 `std
::bind都会生成函数对象。例如,在 GCC 编译器中,Lambda 表达式会被转换为匿名结构,其中重载了
operator()方法。这可以在 GCC 源码的
` 头文件中找到。
4.4 对比 std::bind
和 Lambda 的示例
为了进一步理解两者的差异和优势,让我们考虑以下情况:
特性 | std::bind |
Lambda 表达式 |
---|---|---|
语法简洁性 | 较为复杂,需要使用 std::placeholders |
更加简洁,直观 |
性能 | 在某些编译器上可能稍慢,因为它可能产生额外的函数调用 | 通常更快,因为它在很多情况下可以被内联 |
可读性 | 可能需要查阅文档来理解绑定的参数和占位符 | 更容易阅读,因为它直接在使用的地方定义 |
在编译器中的实现 | 通常作为函数模板实现 | 作为匿名结构实现,重载了 operator() 方法 |
正如某位心理学家所言:“人们对直观和简洁的信息有天生的偏好。” 当我们在选择使用 std::bind
还是 Lambda 表达式时,考虑到这种天生的偏好,以及每种方法的优点和局限性,可以帮助我们做出明智的决策。
4.5 总结
在现代 C++ 编程中,std::bind
和 Lambda 表达式都为我们提供了强大的工具来创建和使用函数对象。虽然 std::bind
在某些情况下可能仍然有其用处,但 Lambda 表达式因其简洁性、直观性和性能优势而越来越受欢迎。通过理解这两个工具的工作原理和用途,我们可以更加有效地使用 C++ 为我们提供的功能。
5. 深入 enqueue
函数:综合应用
在程序设计中,我们常常面临选择如何最有效地组织和执行任务的问题。这是一个不仅涉及技术,还涉及人的思维和决策过程的问题。在此章节中,我们将深入探讨 enqueue
函数,并了解如何利用 C++ 的高级特性对其进行改进。
5.1 回顾原始的 enqueue
函数
首先,我们回顾一下之前的 enqueue
函数实现。它的目的是将一个任务添加到线程池中。
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
using return_type = typename std::result_of<F(Args...)>::type;
auto task = std::make_shared< std::packaged_task<return_type()> >(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
if(stop) {
throw std::runtime_error("enqueue on stopped ThreadPool");
}
tasks.emplace([task](){
(*task)(); });
}
condition.notify_one();
return res;
}
这个函数的核心是创建一个任务,并将其添加到一个待执行的任务队列中。任务是使用 std::bind
创建的,这样,当这个任务在稍后被执行时,它将调用函数 f
并传入参数 args
。
5.2 结合 std::invoke
和 Lambda 的改进
正如Bjarne Stroustrup在《The C++ Programming Language》中所说:“我们不应该因为某样东西是新的、不同的或时髦的而去使用它,而是应该考虑它是否更合适。” 结合这个观点,我们看到 std::bind
在很多情况下实际上并不是最佳选择。与之相反,C++14 和 C++17 提供了更先进的特性,如 lambda 和 std::invoke
,它们为我们提供了更清晰、更直观的工具。
我们先看下使用 lambda 和 std::invoke
重写的 enqueue
函数:
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<std::invoke_result_t<F, Args...>>
{
using return_type = std::invoke_result_t<F, Args...>;
auto task = std::make_shared<std::packaged_task<return_type()>>(
[f = std::forward<F>(f), ...args = std::forward<Args>(args)]() mutable {
return std::invoke(f, args...);
}
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
if(stop) {
throw std::runtime_error("enqueue on stopped ThreadPool");
}
tasks.emplace([task](){
(*task)(); });
}
condition.notify_one();
return res;
}
对比两个版本的 enqueue
函数,我们看到 lambda 结合 std::invoke
提供了一个更清晰和直观的方式来捕获和调用函数。特别是,我们使用了 generalized lambda capture 来捕获函数和参数,然后在 lambda 的主体中使用 std::invoke
来调用函数。这不仅简化了代码,还使其更易于阅读和维护。
5.3 优势与实际应用
5.3.1 更直观的语法
使用 std::invoke
和 lambda,我们可以更自然地表示函数调用,而不需要涉及复杂的 std::bind
语法。对于读者和维护者来说,这意味着更少的认知负担。
5.3.2 更强的类型安全
std::invoke_result_t
提供了一个强类型的方式来确定函数的返回类型,这使我们能够在编译时捕获更多的错误,而不是在运行时。
5.3.3 更好的性能
在某些编译器和设置下,使用 std::invoke
和 lambda 可能比 std::bind
提供更好的性能,尤其是在涉及大量函数调用的情况下。
结论:改进后的 enqueue
函数不仅更清晰、更简洁,而且在某些情况下还可能更高效。这是一个典型的例子,说明了如何通过使用 C++ 的新特性来改进旧代码,使其更易于维护和扩展。
在下一章中,我们将探讨如何进一步优化和扩展 enqueue
函数,以支持更多的用例和功能。
结语
在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。
这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。
我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。
阅读我的CSDN主页,解锁更多精彩内容:泡沫的CSDN主页