[C++ 템플릿 프로그래밍의 실무 방법] C++의 packaged_task, Invoke_result_t, Bind, result_of, Lambda에 대한 심층적인 이해


1. 소개

프로그래밍의 세계에서는 핵심 개념을 이해하고 숙달하는 것이 중요합니다. Bjarne Stroustrup이 "C++ 프로그래밍 언어"에서 말했듯이: "C++는 하드웨어에 대한 강력한 제어를 제공하는 직접적이고 효율적인 언어입니다." 이 기사에서는 일반적으로 사용되는 강력한 도구인 C ++, , 및 Lambda에 대해 자세히 설명 packaged_task합니다 invoke_result_t. 프로그래밍에서.bindresult_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_taskstd::future 就非常有用了。我们可以为每个小块创建一个 std::packaged_task,然后在不同的线程上执行它们。当所有这些任务都完成后,我们可以简单地从每个 std::future 对象中获取结果,并将它们加在一起。

正如 Bjarne Stroustrup 在《The C++ Programming Language》中所说:“C++ 的主要目的是使抽象成为现实。” 在这种情境下,std::packaged_taskstd::future 允许我们将多线程编程的复杂性抽象出来,使其变得更加简单和直观。

此外,如果我们深入到 GCC 的源码中,可以看到 std::packaged_task 是如何实现的。在 libstdc++ 的实现中,std::packaged_task 是定义在 future 头文件中的。它的主要功能是通过 _M_invoke 方法来执行存储的任务。这一切都是在 bits/future.h 文件中实现的。

3. 从 std::result_ofstd::invoke_result_t

在早期的 C++ 标准中,std::result_of 是一个非常有用的工具,它可以帮助我们得知一个函数调用的返回类型。但是,随着时间的推移,我们发现它有一些不足之处,尤其是在新标准的上下文中。因此,C++17 引入了一个新的、更强大的工具:std::invoke_result_t

3.1 为什么需要这些工具?

在模板编程中,我们经常需要知道某个函数或可调用对象的返回类型。这些信息可以帮助我们为函数的输出创建适当的存储、做类型检查或决定如何进一步处理这些输出。

例如,当你有一个函数模板,它的返回类型取决于它的参数,或者当你有一个返回类型是 lambda 的函数。在这些情况下,你不能简单地查看函数的签名来确定它的返回类型,因为这个类型是动态的,取决于实际传递给函数的参数。

这正是 std::result_ofstd::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_ofstd::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主页
在这里插入图片描述

추천

출처blog.csdn.net/qq_21438461/article/details/132850229