[Practical Methods for C++ Template Programming] In-depth understanding of packaged_task, invoke_result_t, bind, result_of and Lambda in C++


1 Introduction

In the world of programming, understanding and mastering core concepts is crucial. As Bjarne Stroustrup said in "The C++ Programming Language": "C++ is a direct and efficient language that provides powerful control over the hardware." This article will delve into C++, , , and packaged_taskLambda invoke_result_t, bindwhich result_ofare It is a powerful tool commonly used in programming.

Whenever we stand at the crossroads of knowledge, we are faced with a choice. Each method, technology or tool has its own unique advantages and applicable scenarios. The method we choose is not just based on its functionality, but also on our mindset and understanding of the problem. As we explore these C++ features, let's understand not just from a technical perspective, but also from a psychological perspective, why certain features are more popular and why their design philosophies are the way they are.

1.1 Rapid Development of C++

C++ is an evolving language. Since its inception, C++ has continued to evolve, providing developers with more tools and greater efficiency. But with the increase in tools and technology comes more complexity. As an old piece of wisdom goes: “Simplicity is not about simplifying, it’s about finding balance in the midst of chaotic complexity.”

In earlier versions of C++, we used tools and techniques that may now seem outdated or inelegant. But just as each generation of craftsmen created with the tools at their disposal, we should also respect these early methods because they laid the foundation for today's progress.

1.2 Theme and importance of this article

The five C++ features discussed in this article are core concepts in modern C++ development. Not only because they provide powerful functions, but more importantly, they represent the philosophy and design principles of C++.

For example, Lambda expressions allow us to express complex operations in a concise and intuitive way without the need to define additional functions or classes. and provide powerful tools for asynchronous programming and functional programming std::bind.std::packaged_task

Before we delve into these features, we need to understand their origins, design goals, and how to use them in real-world programming. In this way, not only can we better understand these features, but we can also learn how to use C++ more effectively.

2. Understandstd::packaged_task

std::packaged_taskis a powerful tool introduced in C++11 that allows us to wrap any callable object (function, lambda, member function pointer, etc.) and cooperate with it to perform asynchronous tasks and retrieve results std::future.

2.1 Definition and main purpose

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主页
在这里插入图片描述

Guess you like

Origin blog.csdn.net/qq_21438461/article/details/132850229