C++快餐——C++11(3)

在这里插入图片描述

应该在脖子上长一个自己的脑袋


function包装器

在这里插入图片描述

C++11引入了一个名为 std::function 的标准函数包装器,也可以叫做适配器,它提供了一种通用的方式来包装可调用对象(函数、函数指针、函数对象、Lambda 表达式等),并可以在需要时进行调用。std::function 是一个模板类,定义在 < functional > 头文件中。示例:

#include <iostream>
#include <functional>

// 被包装的函数
void hello() 
{
    std::cout << "Hello, world!" << std::endl;
}

int main() 
{
    // 将 hello 函数包装为 std::function 对象
    std::function<void()> func = hello;
    
    func();

    return 0;
}

在这里插入图片描述

当使用模板编写函数包装器时,每次使用不同的类型进行实例化时,都会生成一个新的函数实例。这可能导致代码膨胀和额外的开销。为了避免这种情况,可以使用类型擦除技术,将不同类型的可调用对象都存储为相同的类型,从而避免实例化多份。

而这其中的一个常见的类型擦除技术是就使用 std::function,它是一个通用的函数包装器。它可以存储不同类型的可调用对象,而无需生成额外的代码实例化。std::function 使用类型擦除来实现通用的函数封装,可以在运行时根据需要进行调用。例如:

void sum(int x, int y) 
{
    std::cout << "result : " << x + y << std::endl;
}

class sum_func
{
public:
    void operator()(int x, int y)
    {
        std::cout << "result : " << x - y << std::endl;
    }
};

int main() 
{
    auto sum_lambda = [](int x, int y)
    {
        std::cout << "result : " << x * y << std::endl;
    };
    
    sum(10, 7);
    sum_func()(20, 7);
    sum_lambda(30, 5);

    return 0;
}

在这里插入图片描述

假设我们在不同的作用域的不同场景中需要使用到这三个函数时,就需要实例化出三份代码,但是如果使用 function 包装器的话,就可以简洁的实例化一份解决问题。如下:

int main() 
{
    auto sum_lambda = [](int x, int y)
    {
        std::cout << "result : " << x * y << std::endl;
    };

    std::function<void(int, int)> func = sum;
    func = sum_func();
    func = sum_lambda;
    func(10, 7);

    return 0;
}

在这里插入图片描述

std::function 有许多优点,使其成为处理可调用对象的强大工具:

优点说明
通用性和灵活性:std::function 是一个通用的函数封装器,可以存储和调用各种类型的可调用对象,包括函数指针、函数对象、lambda 表达式等。这使得它非常适合处理不同类型的可调用对象,并且可以在不改变代码结构的情况下进行切换和替换。
类型安全std::function 在编译时会对类型进行检查,确保存储的可调用对象与指定的函数签名匹配。这提供了一定程度的类型安全性,避免了在运行时出现类型错误的可能性。
简化接口和回调:通过将可调用对象存储为 std::function,可以将它们作为参数传递给其他函数或类,并在需要时进行调用。这简化了接口的设计,并允许用户在特定事件发生时注册回调函数。
延迟绑定:std::function 允许在运行时绑定可调用对象。这意味着可以将函数的选择推迟到程序运行时,根据动态条件来确定要调用的函数。这在需要根据运行时环境或用户输入动态选择函数的情况下非常有用。
函数组合和封装:std::function 具有函数组合和封装的能力。可以将多个 std::function 对象组合成一个新的函数,实现函数的串联调用或并行调用。此外,std::function 还可以用作其他函数的封装器,将函数的行为进行包装和修改。
可替代模板特化在一些情况下,std::function 可以作为替代模板特化的一种方式。使用模板特化可能导致生成多个函数实例,而 std::function 可以将不同类型的可调用对象都存储为相同的类型,避免了代码膨胀和额外实例化的问题。

总而言之,std::function 提供了一种通用、灵活和类型安全的方式来处理可调用对象。它简化了接口设计、回调注册和函数组合等任务,并能够在运行时进行延迟绑定和动态选择函数。通过这些优点,std::function 成为了处理可调用对象的重要工具之一。

bind

在这里插入图片描述

std::bind 是 C++ 标准库中的一个函数模板,用于创建一个可调用对象,它将参数和函数绑定在一起。std::bind 可以用于将函数与其参数进行绑定,或者将成员函数与对象进行绑定,从而生成一个新的可调用对象。std::bind 也是一个函数模板,它就像一个函数包装器,std::bind 的基本语法如下:

std::bind(Function, Arguments...);

其中,Function 是要绑定的函数或成员函数的名称,Arguments… 是要传递给函数的参数。

绑定普通函数

使用bind绑定普通函数代码如下:

#include <iostream>
#include <functional>

void add(int a, int b) 
{
    std::cout << a + b << std::endl;
}

int main() 
{
    auto addFunc = std::bind(add, 10, 20);
    addFunc();  

    return 0;
}

在这个示例中,我们使用 std::bind 将函数 add 与参数 10 和 20 进行绑定,生成了一个新的可调用对象 addFunc。当我们调用 addFunc() 时,实际上调用了绑定的函数 add,并传递了绑定的参数 10 和 20,输出结果为 30。

在这里插入图片描述

如果不想在绑定的时候将参数定死,还可以使用placeholders命名空间中的占位符来让后期指定

在这里插入图片描述
如下代码:

#include <iostream>
#include <functional>

void add(int a, int b) 
{
    std::cout << a + b << std::endl;
}

int main() 
{
    auto addFunc = std::bind(add, placeholders::_1, placeholders::_2);
    addFunc(20, 34);  

    return 0;
}

在这里插入图片描述

绑定成员函数

除了绑定普通函数之外,还可以使用bind来绑定类成员函数,如下:

#include <iostream>
#include <functional>

class MyClass 
{
public:
    void hello(const std::string& message, const std::string& name)
    {
        std::cout << message  << " , " << name << std::endl;
    }
};

int main() 
{
    MyClass obj;
    auto printFunc = std::bind(&MyClass::hello, &obj, placeholders::_1, placeholders::_2);
    printFunc("Hello", "Jack");

    return 0;
}

在这个示例中,我们定义了一个类 MyClass,其中包含一个成员函数 hello。我们使用 std::bind 将成员函数 hello与对象 obj 以及占位符进行绑定,生成了一个新的可调用对象 printFunc。当我们调用 printFunc() 时,实际上调用了绑定的成员函数 hello,并传递了绑定的对象 obj 和替换占位符的参数。

需要注意的是,当绑定成员函数时,必须使用成员函数的指针,并将对象的指针或引用作为第一个参数传递给 std::bind。

在这里插入图片描述

占位符还可以交换,但是不可以跳跃。

在这里插入图片描述

auto printFunc = std::bind(&MyClass::hello, &obj, placeholders::_2, placeholders::_1);,输出结果如下:

在这里插入图片描述

std::bind优点

std::bind 具有以下几个优点:

优点说明
参数绑定:std::bind 允许将函数的部分参数进行绑定,即在创建可调用对象时可以指定部分参数的值,而无需提供所有参数。这使得在后续调用时可以更方便地使用绑定的可调用对象,只需提供未绑定的参数即可。参数绑定提供了一种延迟执行函数的能力,非常有用。
灵活性std::bind 允许将函数与参数绑定在一起,也可以将成员函数与对象进行绑定。这种灵活性使得我们可以在不改变原有函数或成员函数的情况下,创建新的可调用对象,可以根据实际需求进行参数绑定和对象绑定。
函数适配器std::bind 提供了一些函数适配器,例如 std::placeholders::_1、std::placeholders::_2 等,用于占位符参数。这些占位符可以在创建可调用对象时使用,并在后续调用时动态填充实际参数。函数适配器增强了 std::bind 的灵活性和可用性。
代码重用和简化std::bind 可以用于将函数或成员函数与特定的参数或对象进行绑定,生成新的可调用对象。这样可以避免重复编写相似的代码,提高代码的重用性。同时,它简化了接口设计和回调注册的过程,使代码更加简洁和易读。
延迟绑定和动态调用std::bind 允许在运行时进行参数绑定,并生成可调用对象。这使得我们可以根据运行时的条件或需求来动态选择要调用的函数或成员函数。这种延迟绑定和动态调用的能力非常有用,可以提高代码的灵活性和可扩展性。

总而言之,std::bind 提供了一种灵活、可定制和方便的方式来创建可调用对象,并在需要的时候进行参数绑定。它可以用于函数的部分参数绑定、成员函数的对象绑定,以及使用函数适配器进行占位符参数的处理。通过这些优点,std::bind 提供了更多的编程选项和技巧,使代码更加灵活和可重用。

线程库

C++ 线程库是 C++11 标准引入的一个库,提供了一组用于多线程编程的类和函数,旨在简化并发编程的实现。它以 std::thread 为核心,还包括其他类和函数,用于线程管理、线程同步和线程间通信。C++ 线程库的主要组件和功能如下

组件功能
std::thread 类:std::thread 是 C++ 线程库的核心类,用于创建和管理线程。通过创建 std::thread 对象,可以在新的线程中执行指定的函数或可调用对象。它提供了函数来控制线程的启动、加入(join)、分离(detach)和线程标识等操作。
线程同步原语C++ 线程库提供了多种线程同步原语,用于协调不同线程之间的操作。其中包括互斥量(std::mutex)、条件变量(std::condition_variable)、原子操作(std::atomic)等。这些同步原语可以用于实现线程间的互斥访问、条件等待和原子操作,确保线程安全和协同工作。
std::async 和 std::future:std::async 和 std::future 是 C++ 线程库提供的用于异步编程的工具。std::async 可以在后台启动一个异步任务,并返回一个 std::future 对象,用于获取异步任务的结果。std::future 可以用于等待异步任务的完成,并获取其结果,或者通过 std::future 的异步操作来检查任务的状态。
std::atomic 类std::atomic 类是 C++ 线程库提供的用于原子操作的模板类。它提供了原子类型的操作函数,用于实现并发环境下的安全数据访问。通过 std::atomic,可以避免竞争条件和数据竞争,确保多线程环境下的数据一致性。
std::mutex 类:std::mutex 是 C++ 线程库提供的互斥量类,用于实现线程间的互斥访问。它提供了锁和解锁操作,用于保护临界区代码,避免多个线程同时访问共享资源导致的竞争条件和数据竞争。
std::condition_variable 类std::condition_variable 是 C++ 线程库提供的条件变量类,用于线程间的条件等待和通知。它配合互斥量使用,可以实现线程的等待和唤醒机制,用于线程间的同步和协调。

C++ 线程库提供了丰富的功能和工具,使得多线程编程更加方便和安全。它简化了线程的创建、管理和同步,提供了一组统一的接口和工具,用于处理多线程环境下的并发操作。通过这些组件,开发人员可以更轻松地实现并发算法、多线程任务和并行计算等应用。

thread类

在这里插入图片描述

std::thread 是 C++ 标准库中提供的类,用于创建和管理线程。它是 C++ 线程库的核心组件之一,位于 < thread > 头文件中。使用 std::thread,可以在新的线程中执行指定的函数或可调用对象。

创建线程

可以通过 std::thread 的构造函数创建线程。构造函数接受一个可调用对象(函数指针、函数对象或 lambda 表达式等)作为参数,并在新的线程中执行该可调用对象。还可以通过在 std::thread 构造函数中传递参数,将参数传递给线程的执行函数。
在这里插入图片描述

void threadFunction(int value) 
{
    // 线程执行的代码,可以使用传递的参数
}

int main() 
{
    int data = 42;
    std::thread t(threadFunction, data);  // 传递参数给线程函数
    // ...
    t.join();  // 等待线程结束
    return 0;
}

线程管理

std::thread 提供了一些函数来管理线程的行为。其中包括 join() 和 detach() 函数。join() 函数用于等待线程执行完毕。调用 join() 后,主线程将被阻塞,直到被调用的线程完成执行。
在这里插入图片描述

void threadFunction() 
{
    // 线程执行的代码
}

int main() 
{
    std::thread t(threadFunction);  // 创建线程并指定可调用对象
    // ...
    t.join();  // 等待线程结束
    return 0;
}

detach() 函数用于分离线程。调用 detach() 后,线程将在后台继续执行,主线程不再等待该线程的结束。detach后就不可以join了
在这里插入图片描述

void threadFunction() 
{
    // 线程执行的代码
}

int main() 
{
    std::thread t(threadFunction);  // 创建线程
    // ...
    t.detach();  // 分离线程,主线程不再等待
    return 0;
}

线程标识

std::thread 对象具有一个唯一的线程标识,可以通过 std::thread::id 类型的成员函数 get_id() 获取线程的标识。
在这里插入图片描述

void threadFunction() {
    // 线程执行的代码
}

int main() {
    std::thread t(threadFunction);  // 创建线程
    std::thread::id threadId = t.get_id();  // 获取线程标识
    // ...
    t.join();  // 等待线程结束
    return 0;
}

需要注意的是,当 std::thread 对象被销毁时,如果线程仍在运行且未被分离(未调用 detach()),则会导致程序终止。因此,确保在线程结束之前调用 join() 或 detach() 来管理线程的生命周期。std::thread 还提供了其他函数和操作,例如 native_handle() 函数用于获取底层操作系统的线程句柄,hardware_concurrency() 函数用于获取系统支持的并发线程数量等。且线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此即使线程参数为引用类型,在
线程中修改后也不会修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

通过 std::thread,可以方便地创建和管理线程,实现并发编程。它为多线程操作提供了一个高级的 C++ 接口,简化了线程的创建、管理和同步,提高了并发编程的可读性和可维护性。

原子性操作库(atomic)

在这里插入图片描述

当程序涉及多线程时,随之而来的就是线程安全问题,当多个线程对同一个数据进行修改时,非常容易引发线程安全问题,在C++11之前的解决方法通常是使用加锁和解锁方法,但是在加锁和解锁这段过程之间只能有一个线程进行访问,其他线程都会被阻塞在外面,影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。因此C++11中引入了原子操作,即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。

C++ 提供的用于原子操作的 std::atomic 库位于 头文件中。std::atomic 提供了一组模板类和函数,用于定义和操作原子类型的对象。以下是 std::atomic 库的主要特点和使用方法:

  • 原子类型: std::atomic 支持多种原子类型,如整数类型(int、unsigned int、std::size_t 等)、指针类型和布尔类型等。可以使用 std::atomic 来定义原子类型的对象,其中 T 是要原子化的数据类型。
std::atomic<int> counter(0);  // 定义一个原子整数对象
  • 原子操作: std::atomic 提供了一组原子操作函数,用于对原子对象进行读取、写入和修改等操作。这些操作函数保证在多线程环境中的原子性,避免了竞争条件和数据竞争。
    load():原子读取操作,获取原子对象的当前值。
    store():原子写入操作,修改原子对象的值。
    exchange():原子交换操作,将新值赋予原子对象,并返回原来的值。
    fetch_add() 和 fetch_sub():原子加法和减法操作,将指定值加到原子对象上或从原子对象中减去指定值,并返回原子对象原来的值。
std::atomic<int> counter(0);
int value = counter.load();  // 原子读取操作
counter.store(10);  // 原子写入操作
int oldValue = counter.exchange(5);  // 原子交换操作
int result = counter.fetch_add(3);  // 原子加法操作
  • 原子性操作: std::atomic 提供了原子性操作的保证,确保多个线程对同一个原子对象的操作是互斥的。这意味着在原子操作期间,不会发生数据竞争。

  • 内存顺序: std::atomic 还支持指定内存顺序,用于控制原子操作的内存可见性和排序方式。可以使用 std::memory_order 枚举类型来指定内存顺序,如 std::memory_order_relaxed、std::memory_order_acquire、std::memory_order_release 等。

std::atomic<int> counter(0);

counter.fetch_add(1, std::memory_order_relaxed);  // 松散的内存顺序
counter.fetch_add(1, std::memory_order_acquire);  // 获取顺序
counter.fetch_add(1, std::memory_order_release);  // 释放顺序

通过 std::atomic,可以实现线程安全的原子操作,避免竞争条件和数据竞争,确保多线程环境下的数据一致性。它为并发编程提供了一种高效且可靠的方式,用于处理共享数据的并发访问和修改。

lock_guard与unique_lock

std::lock_guard 和 std::unique_lock 是 C++ 标准库中用于提供互斥量(mutex)的 RAII封装类。它们用于管理互斥量的加锁和解锁操作,确保在作用域结束时自动释放锁,避免忘记手动解锁而导致的死锁或数据竞争。

std::lock_guard

在这里插入图片描述

std::lock_guard 是一个模板类,通过在构造函数中传入互斥量(std::mutex 或其派生类)对象,可以自动加锁,而在析构函数中自动解锁。它适用于简单的加锁和解锁操作,没有额外的灵活性。

std::mutex mtx;

void foo()
{
    std::lock_guard<std::mutex> lock(mtx); // 自动加锁
    // 执行受保护的代码
} // 自动解锁

std::unique_lock

在这里插入图片描述

std::unique_lock 也是一个模板类,与 std::lock_guard 类似,可以管理互斥量的加锁和解锁操作。不同之处在于 std::unique_lock 提供了更多的灵活性和可配置性。可以在构造函数中传入互斥量对象,并指定锁定类型和内存顺序。

std::mutex mtx;

void foo()
{
    std::unique_lock<std::mutex> lock(mtx); // 自动加锁
    // 执行受保护的代码
    lock.unlock(); // 手动解锁
    // 执行其他操作
    lock.lock(); // 再次加锁
    // 执行受保护的代码
} // 自动解锁

std::unique_lock 还提供了以下特性:

  • 支持延迟加锁和条件变量:可以在不加锁的情况下创建 std::unique_lock 对象,并在需要时手动加锁。
  • 支持对互斥量的所有权转移:可以将 std::unique_lock 对象的所有权从一个线程转移到另一个线程,实现更灵活的锁管理。

总的来说,std::lock_guard 简单且直观,适用于大多数情况下的互斥量管理。而 std::unique_lock 提供了更多的灵活性和可配置性,适用于需要更复杂锁定策略和条件变量的情况。选择使用哪个类取决于具体需求和使用场景。

mutex的种类

C++ 标准库中提供了几种互斥量(mutex)的类型,用于实现线程间的互斥访问和同步。下面是常见的互斥量类型:

类型说明
std::mutexstd::mutex 是最基本的互斥量类型,提供了最基本的加锁和解锁操作。它是非递归的互斥量,意味着同一个线程无法多次对同一个互斥量进行加锁,否则会导致死锁。多个线程可以通过 std::mutex 实现对共享资源的互斥访问。
std::recursive_mutexstd::recursive_mutex 是递归互斥量,允许同一个线程多次对互斥量进行加锁。递归互斥量会跟踪每个线程对互斥量的加锁次数,在解锁时需要相应数量的解锁操作。递归互斥量适用于需要递归调用的情况,但需要注意避免死锁。
std::timed_mutexstd::timed_mutex 是具有超时功能的互斥量。它提供了 try_lock_for() 和 try_lock_until() 成员函数,可以尝试在一段时间内加锁,如果加锁失败则返回。这对于需要在一定时间内获取锁的场景非常有用。
std::recursive_timed_mutexstd::recursive_timed_mutex 是具有超时功能的递归互斥量。它结合了 std::recursive_mutex 和 std::timed_mutex 的特性,可以在一定时间内递归地尝试加锁。
std::shared_mutexstd::shared_mutex 是读写锁(shared_mutex),也称为共享互斥量。它支持共享读取和独占写入模式。多个线程可以同时获取读取锁,但只有一个线程可以获取写入锁。读取锁可以被多个线程同时持有,写入锁则独占。这对于读多写少的场景可以提供更好的性能。

这些互斥量类型都定义在 头文件中,可以通过它们来实现线程之间的互斥访问和同步。选择合适的互斥量类型取决于具体的应用场景和需求。

总结

文章介绍了C++11中的包装器以及bind函数模板,还对C++11中添加的线程库进行介绍,并对多线程操作产生的线程安全问题进行分析解决,引入了原子操作库以及互斥量mutex。这些知识点对于帮助我们写出高质量C++程序的用处极大,如果文章中有哪些内容能够帮助到你的话也算是文章有其所存在的价值,劳烦点个赞就当是对博主的肯定呗!

在这里插入图片描述

  • 12
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拖拉机厂第一代码手

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值