C++11的多线程、function和bind、可变函数模板

文章目录

  • 一、C++11多线程thread
    • 1.1 线程thread
      • 1.1.1 构造函数
      • 1.1.2 主要成员函数
      • 1.1.3 范例示范
    • 1.2 互斥量
      • 1.2.1 独占互斥量std::mutex
      • 1.2.2 范例
      • 1.2.3 lock_guard和unique_lock的使用和区别
    • 1.3 条件变量
      • 1.3.1 wait函数
      • 1.3.2 wait_for函数
      • 1.3.3 wait_until函数
      • 1.3.4 notify_one函数
      • 1.3.5 notify_all函数
    • 1.4 异步操作
      • 1.4.1 std::aysnc和std::future
      • 1.4.2 std::packaged_task
      • 1.4.3 std::promise
  • 二、function和bind用法
    • 2.1 function
    • 2.2 bind
  • 三、可变参数模板
    • 3.1 语法
    • 3.2 递归函数来展开参数包
    • 3.3 逗号表达式展开参数包
    • 3.4 折叠表达式

一、C++11多线程thread

1.1 线程thread

std::thread 在 #include 头文件中声明,因此使用 std::thread 时需要包含 #include 头文件。

1.1.1 构造函数

1、默认构造函数

//创建一个空的 thread 执行对象。
thread() _NOEXCEPT
   {   // construct with no thread
    _Thr_set_null(_Thr);
   }

2、初始化构造函数

//创建std::thread执行对象,该thread对象可被joinable,新产生的线程会调用threadFun函数,该函数的参数由 args 给出
template<class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args);

3、拷贝构造函数

// 拷贝构造函数(被禁用),意味着 thread 不可被拷贝构造。
thread(const thread&) = delete;
thread t1;
thread t2 =t1; // 错误

4、Move构造函数

//move 构造函数,调用成功之后, x 不代表任何 thread 执行对象。
//注意:可被 joinable 的 thread 对象必须在他们销毁之前被主线程 join 或者将其设置为detached。
thread(thread&& x)noexcept
 
thread t1;
thread t2 =move(t1); // 可以

5、范例

#include
#include
using namespace std;

void threadFun(int &a) // 引用传递
{
 cout << "this is thread fun !" <<endl;
 cout <<" a = "<<(a+=10)<<endl;
}
int main()
{
 int x = 10;
 thread t1(threadFun, std::ref(x));     // std::ref(x) 创建了对变量 x 的引用。
 thread t2(std::move(t1)); // t1 线程失去所有权
 thread t3;
 t3 = std::move(t2); // t2 线程失去所有权

 t3.join();
 cout<<"Main End "<<"x = "<<x<<endl;
 return 0;
}
this is thread fun !
 a = 20
Main End x = 20

1.1.2 主要成员函数

  • get_id()
    获取线程ID,返回类型std::thread::id对象。
    http://www.cplusplus.com/reference/thread/thread/get_id/

- joinable()
判断线程是否可以加入等待
http://www.cplusplus.com/reference/thread/thread/joinable/

  • join()
    等该线程执行完成后才返回。
    http://www.cplusplus.com/reference/thread/thread/join/

  • detach()
    detach调用之后,目标线程就成为了守护线程,驻留后台运行,与之关联的std::thread对象失去对目标线程的关联,无法再通过std::thread对象取得该线程的控制权。当线程主函数执行完之后,线程就结束了,运行时库负责清理与该线程相关的资源。
    调用 detach 函数之后:
    1)*this 不再代表任何的线程执行实例。
    2)joinable() == false
    3)get_id() == std::thread::id()
    http://www.cplusplus.com/reference/thread/thread/detach/

1.1.3 范例示范

#include 
#include  // 头文件
using namespace std;

// 1 传入0个值
void func1()
{
    cout << "func1 into" << endl;
}


// 2 传入2个值
void func2(int a, int b)
{
    cout << "func2 a + b = " << a+b << endl;
}

void func2_1(int a, int b)
{
    cout << "func2_1 a + b = " << a+b << endl;
}

int func2_1(string a, string b)
{
    cout << "func2_1 a + b = " << a << b<< endl;
    return 0;
}

// 3 传入引用
void func3(int &c) // 引用传递
{
    cout << "func3 c = " << &c << endl;
    c += 10;
}



//
class A
{
public:
//    4. 传入类函数
    void func4(int a)
    {
//        std::this_thread::sleep_for(std::chrono::seconds(1));
        cout << "thread:" << name_<< ", fun4 a = " << a << endl;

    }
    void func4(string str)
    {
//        std::this_thread::sleep_for(std::chrono::seconds(1));
        cout << "thread:" << name_<< ", fun4 str = " << str << endl;

    }
    void setName(string name) {
        name_ = name;
    }
    void displayName() {
        cout << "this:"  << this << ", name:" << name_ << endl;
    }
    void play()
    {
        std::cout<<"play call!"<<std::endl;
    }
private:
    string name_;
};

//5. detach
void func5()
{
    cout << "func5 into sleep " <<  endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    cout << "func5 leave " <<  endl;
}

// 6. move
void func6()
{
    cout << "this is func6 !" <<endl;
}

mian函数里分别调用下面的各种情况
1)传入0个值

//    1. 传入0个值
   cout << "\n\n main1--------------------------\n";
   std::thread t1(func1);  // 只传递函数
   t1.join();  // 阻塞等待线程函数执行结束
 main1--------------------------
func1 into

2)传入2个值
第一种:

   int a =10;
   int b =20;
   std::thread t2(func2, a, b); // 加上参数传递,可以任意参数
   t2.join();
func2 a + b = 30

第二种:

    int a =10;
    int b =20;
    std::thread t2((void(*)(int, int)) func2_1, a, b); // 加上参数传递,可以任意参数
    t2.join();
func2_1 a + b = 30

第三种

    std::thread t2_2((int(*)(string, string)) func2_1, "zxm", " and mark"); // 加上参数传递,可以任意参数
    t2_2.join();
func2_1 a + b = zxm and mark

3)传入引用

   int c =10;
   std::thread t3(func3, std::ref(c)); // std::ref 加上参数传递,可以任意参数
   t3.join();
   cout << "main3 c = " << &c << ", "<<c << endl;
func3 c = 0x61fd34
main3 c = 0x61fd34, 20

4)传入类函数
重载1

   A * a4_ptr2 = new A();
    a4_ptr2->setName("king");
    std::thread t41((void(A::*)(int))&A::func4, a4_ptr2, 100);  // 重载void func4(int a),(A::*) 表示这是一个成员函数指针
    t41.join();
    delete a4_ptr2;
thread:king, fun4 a = 100

重载2

    A * a4_ptr3 = new A();
    a4_ptr3->setName("Darren");
    std::thread t43((void(A::*)(string))&A::func4, a4_ptr3, "and zxm"); // 重载 int func4(string str)
    t43.join();
    delete a4_ptr3;
thread:Darren, fun4 str = and zxm

5)detach

   std::thread t5(&func5);  // 只传递函数
   t5.detach();  // 脱离
    cout << "pid: " << t5.get_id() << endl; // t5此时不能管理线程了
   cout << "joinable: " << t5.joinable() << endl; // false
   // t5.join(); 这里不能用join了,会直接崩掉
   cout << "\n main5 end\n";
func5 into sleep 
pid: thread::id of a non-executing thread
joinable: 0

 main5 end

6)move

    int x = 10;
   thread t6_1(func6);
   thread t6_2(std::move(t6_1)); // t6_1 线程失去所有权
    // t6_1.join();  // 抛出异常   after throwing an instance of 'std::system_error'
    t6_2.join();
this is func6 !

1.2 互斥量

mutex又称互斥量,C++ 11中与 mutex相关的类(包括锁类型)和函数都声明在 头文件中,所以如果你需要使用 std::mutex,就必须包含 头文件。

C++11提供如下4种语义的互斥量(mutex)
1)std::mutex,独占的互斥量,不能递归使用。
2)std::time_mutex,带超时的独占互斥量,不能递归使用。
3)std::recursive_mutex,递归互斥量,不带超时功能。
4)std::recursive_timed_mutex,带超时的递归互斥量。

1.2.1 独占互斥量std::mutex

std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性— —即不支持递归地对std::mutex对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。
(可重入互斥锁(Reentrant Mutex),也称为递归互斥锁,是一种特殊类型的互斥锁。它允许同一线程多次获得该锁而不会产生死锁。)

std::mutex 的成员函数
1)构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于unlocked状态的。

2)lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:

∙ \bullet 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
∙ \bullet 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
∙ \bullet 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

3)unlock(), 解锁,释放对互斥量的所有权。

4)try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况
∙ \bullet 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
∙ \bullet 如果当前互斥量被其他线程锁住,则当前调用线程返回false,而并不会被阻塞掉。
∙ \bullet 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

1.2.2 范例

#include        // std::cout
#include          // std::thread
#include           // std::mutex

volatile int counter(0); // non-atomic counter
std::mutex mtx;           // locks access to counter

void increases_10k()
{
    for (int i=0; i<10000; ++i) {
        // 1. 使用try_lock的情况
       if (mtx.try_lock()) {   // only increase if currently not locked:
           ++counter;
           mtx.unlock();
       }
        // 2. 使用lock的情况
                // {
                //     mtx.lock();
                //     ++counter;
                //     mtx.unlock();
                // }
    }
}

int main()
{
    std::thread threads[10];
    for (int i=0; i<10; ++i)
        threads[i] = std::thread(increases_10k);

    for (auto& th : threads) th.join();
    std::cout << " successful increases of the counter "  << counter << std::endl;

    return 0;
}

使用try_lock的情况,不会阻塞

 successful increases of the counter 15533

使用lock的情况,会阻塞

 successful increases of the counter 100000

1.2.3 lock_guard和unique_lock的使用和区别

1、unique_lock,lock_guard的使用
自动释放锁

#include        // std::cout
#include          // std::thread
#include           // std::mutex, std::lock_guard
#include       // std::logic_error

std::mutex mtx;

void print_even (int x) {
    if (x%2==0) std::cout << x << " is even\n";
    else throw (std::logic_error("not even"));
}

void print_thread_id (int id) {
    try {
//        这里的lock_guard换成unique_lock是一样的。
        // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:
//        std::lock_guard lck (mtx);
        std::unique_lock<std::mutex> lck (mtx);
        print_even(id);
    }
    catch (std::logic_error&) {
        std::cout << "[exception caught]\n";
    }
}

int main ()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i=0; i<10; ++i)
        threads[i] = std::thread(print_thread_id,i+1);

    for (auto& th : threads) th.join();

    return 0;
}

[exception caught]
2 is even
[exception caught]
4 is even
[exception caught]
6 is even
[exception caught]
8 is even
[exception caught]
10 is even

主要看std::unique_lock lck (mtx);,以往我们都是

mtx.lock();
print_even(id);
mtx.unlock();

需要我们手动释放锁,但在if else 这种多return分支中,很容易忘记释放。
std::lock_guard lck (mtx);lck是在栈上的,当退出作用域后会调用析构函数,自动释放锁。
std::unique_lock lck (mtx);也类似,能自动释放锁。

2、unique_lock,lock_guard的区别
1)unique_locklock_guard都能实现自动加锁和解锁,但是前者更加灵活,能实现更多的功能。
2)unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行解锁,lck.lock()进行上锁,而不必等到析构时自动解锁。

#include 
#include 
#include 
#include 
#include 
#include 

std::deque<int> q;
std::mutex mu;
std::condition_variable cond;
int count = 0;

void fun1() {
    while (true) {

        std::unique_lock<std::mutex> locker(mu); 
        std::cout << "fun1 lock\n";
        q.push_front(count++);
        locker.unlock();        // lock_guard是没有手动释放锁的 unlock

        cond.notify_one();  
        sleep(1);
    }
}

void fun2() {
    while (true) {
        std::unique_lock<std::mutex> locker(mu);
        std::cout << "fun2 lock\n";
        std::cout << "fun2 wait into\n";
        cond.wait(locker, [](){return !q.empty();});
        std::cout << "fun2 wait leave\n";
        auto data = q.back();
        q.pop_back();
        locker.unlock(); 
        std::cout << "thread2 get value form thread1: " << data << std::endl;
    }
}
int main() {
    std::thread t1(fun1);
    std::thread t2(fun2);
    t1.join();
    t2.join();
    return 0;
}

上面代码中fun1插入数据(生产者),fun2pop出数据(消费者)。每次加锁之后,需要临时解锁,否则会造成死锁,即

std::unique_lock<std::mutex> locker(mu); 
…… 
locker.unlock();        // lock_guard是没有手动释放锁的 unlock

但是如果换成lock_guard,那就不行了,因此lock_guard是没有手动释放锁的 unlock接口。
当然,也可以加大括号的方式,出了大括号就相当于出了作用域。这时候两个都可以,因为不用unlock

    while (true) {
        {
            std::lock_guard<std::mutex> locker(mu); 
            std::cout << "fun1 lock\n";
            q.push_front(count++);
        }
        cond.notify_one();  
        sleep(1);
    }

1.3 条件变量

互斥量是多线程间同时访问某一共享变量时,保证变量可被安全访问的手段。但单靠互斥量无法实现线程的同步。线程同步是指线程间需要按照预定的先后次序顺序进行的行为。C++11对这种行为也提供了条件变量。条件变量位于头文件condition_variable下。

条件变量使用过程:
1)拥有条件变量的线程获取互斥量;
2)循环检查某个条件,如果条件不满足则阻塞直到条件满足;如果条件满足则向下执行;
3)某个线程满足条件执行完之后调用notify_onenotify_all唤醒一个或者所有等待线程。

1.3.1 wait函数

void wait (unique_lock<mutex>& lck);

template <class Predicate>
 void wait (unique_lock<mutex>& lck, Predicate pred);

包含两种重载,第一种只包含unique_lock对象,另外一个Predicate 对象(等待条件),这里必须使用unique_lock,因为wait函数的工作原理:
1)当前线程调用wait()后将被阻塞并且函数会解锁互斥量,直到另外某个线程调用notify_one或者notify_all唤醒当前线程;一旦当前线程获得通知(notify),wait()函数也是自动调用lock(),同理不能使用lock_guard对象。

2)如果wait()没有第二个参数,第一次调用默认条件不成立,直接解锁互斥量并阻塞到本行,直到某一个线程调用notify_onenotify_all为止,被唤醒后,wait()重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后无条件地继续进行后面的操作。

3)如果wait()包含第二个参数,如果第二个参数不满足,那么wait()将解锁互斥量并堵塞到本行,直到某一个线程调用notify_onenotify_all为止,被唤醒后,wait()重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后继续判断第二个参数,如果表达式为false,wait()对互斥量解锁,然后休眠,如果为true,则进行后面的操作。

1.3.2 wait_for函数

template <class Rep, class Period>
 cv_status wait_for (unique_lock<mutex>& lck,
           const chrono::duration<Rep,Period>& rel_time);
           
template <class Rep, class Period, class Predicate>
   bool wait_for (unique_lock<mutex>& lck,
           const chrono::duration<Rep,Period>& rel_time, Predicate pred);

wait不同的是,wait_for可以执行一个时间段,在线程收到唤醒通知或者时间超时之前,该线程都会处于阻塞状态,如果收到唤醒通知或者时间超时,wait_for返回,剩下操作和wait类似。

1.3.3 wait_until函数

template <class Clock, class Duration>
 cv_status wait_until (unique_lock<mutex>& lck,
            const chrono::time_point<Clock,Duration>& abs_time);
template <class Clock, class Duration, class Predicate>
   bool wait_until (unique_lock<mutex>& lck,
            const chrono::time_point<Clock,Duration>& abs_time, Predicate pred);

wait_for类似,只是wait_until可以指定一个时间点,在当前线程收到通知或者指定的时间点超时之前,该线程都会处于阻塞状态。如果超时或者收到唤醒通知,wait_until返回,剩下操作和wait类似

1.3.4 notify_one函数

void notify_one() noexcept;

解锁正在等待当前条件的线程中的一个,如果没有线程在等待,则函数不执行任何操作,如果正在等待的线程多余一个,则唤醒的线程是不确定的(随机唤醒一个)。

1.3.5 notify_all函数

void notify_all() noexcept;

解锁正在等待当前条件的所有线程,如果没有正在等待的线程,则函数不执行任何操作

1.4 异步操作

  • std::future : 异步指向某个任务,然后通过future特性去获取任务函数的返回结果。
  • std::aysnc: 异步运行某个任务函数
  • std::packaged_task :将任务和feature绑定在一起的模板,是一种封装对任务的封装。
  • std::promise

1.4.1 std::aysnc和std::future

std::future用于在多线程编程中处理异步操作的结果。它提供了一种机制来获取异步任务的返回值或异常,并在需要时等待异步任务完成。

线程可以周期性的在这个future上等待一小段时间,检查future是否已经ready,如果没有,该线程可以先去做另一个任务。一旦future就绪,该future就无法复位(无法再次使用这个future等待这个事件),所以future代表的是一次性事件。

future的类型
在库的头文件中声明了两种future,唯一futurestd::future)和共享futurestd::shared_future)。

这两个是参照std::unique_ptrstd::shared_ptr设立的,前者的实例是仅有的一个指向其关联事件的实例。而后者可以有多个实例指向同一个关联事件,当事件就绪时,所有指向同一事件的std::shared_future实例会变成就绪

future的使用

std::future是一个模板,例如std::future,模板参数就是期待返回的类型,虽然future被用于线程间通信,但其本身却并不提供同步访问。

future使用的时机是当你不需要立刻得到一个结果的时候,你可以开启一个线程帮你去做一项任务,并期待这个任务的返回。但是std::thread并没有提供这样的机制,这就需要用到std::asyncstd::future

std::async返回一个std::future对象,而不是给你一个确定的值。当你需要使用这个值的时候,对future使用get(),线程就会阻塞直到future就绪,然后返回该值.

#include 
#include 
#include 
using namespace std;

int find_result_to_add()
{
    std::this_thread::sleep_for(std::chrono::seconds(5)); // 用来测试异步延迟的影响
    std::cout << "find_result_to_add" << std::endl;
    return 1 + 1;
}
int find_result_to_add2(int a, int b)
{
    std::this_thread::sleep_for(std::chrono::seconds(5)); // 用来测试异步延迟的影响
    return a + b;
}

void do_other_things()
{
    std::cout << "do_other_things" << std::endl;
}
int main()
{
   std::future<int> result = std::async(find_result_to_add);  // async把find_result_to_add交给其他线程异步运行,不会阻塞do_other_things();的运行
// 另外两种声明方式,推荐第三中
// std::future result = std::async(find_result_to_add); // decltype自动推导参数类型
// auto result = std::async(find_result_to_add);  // 推荐的写法
   do_other_things();
   std::cout << "result: " << result.get() << std::endl;  // get会阻塞等待任务函数的返回值

//    std::future result2 = std::async(find_result_to_add2, 10, 20); //错误
    std::future<decltype (find_result_to_add2(0, 0))> result2 = std::async(find_result_to_add2, 10, 20);
    std::cout << "result2: " << result2.get() << std::endl;  // 延迟是否有影响?
    return 0;
}

上面代码中,std::async(find_result_to_add)把函数find_result_to_add异步运行,不会阻塞主线程。当需要返回值时候,再通过result.get()返回。

1.4.2 std::packaged_task

std::packaged_task用于将可调用对象(如函数、Lambda 表达式)封装成一个具备异步执行能力的任务对象。它可以帮助我们将任务的执行与结果的获取分离开来,以便在需要时获取任务的返回值。

使用 std::packaged_task 将可调用对象封装成一个任务对象 task。通过调用 get_future() 方法,我们获取了与任务关联的std::future对象 result,用于获取任务的返回值。

#include 
#include 
#include 

using namespace std;

int add(int a, int b, int c)
{
    std::cout << "call add\n";
    return a + b + c;
}

void do_other_things()
{
    std::cout << "do_other_things" << std::endl;
}

int main()
{
    std::packaged_task<int(int, int, int)> task(add);  // 1. 封装任务,还没有运行,意味着不会阻塞
    
    do_other_things();
    std::future<int> result = task.get_future(); // 2.获取 future,注意这里也不运行
    
    task(1, 1, 2);   //3.这里才真正运行。并且必须要让任务执行,否则在get()获取future的值时会一直阻塞
    std::cout << "result:" << result.get() << std::endl;
    return 0;
}

1.4.3 std::promise

用于在一个线程中产生某个值,并在另一个线程中获取该值。它提供了一种线程之间传递数据的方式,通过 std::promise 和 std::future 的结合使用,可以实现线程间的数据传递和同步等功能。

#include 
#include 
#include 
#include 
using namespace std;
void print1(std::promise<std::string>& p)
{
    std::cout << "print1 sleep" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    p.set_value("set string"); // 履行承诺,设置返回值
}

void do_some_other_things()
{
    std::cout << "do_some_other_things" << std::endl;
}

int main()
{
    std::cout << "main1 -------------" << std::endl;
    // 1.创建了一个 std::promise 对象 promise,用于承诺将计算结果传递给后续处理。
    std::promise<std::string> promise;  // 注意类型:

    // 2.调用 get_future() 方法,我们获取了与承诺关联的 std::future 对象 result,用于获取计算结果。
    std::future<std::string> result = promise.get_future(); // future

    // 3.创建了一个新线程 worker,并将承诺对象 promise 传递给 print1 函数
    std::thread t(print1, std::ref(promise));  // 线程设置 传引用std::ref

    do_some_other_things();

    // 4.等待子线程完成
    t.join();

    std::cout << "wait get result" << std::endl;
    // 5.在其他操作完成后,我们使用 get() 函数从 result 中获取计算结果。
    // 如果结果还没有被设置(即承诺还没有被履行),调用 get() 会导致当前线程阻塞,直到结果可用为止。
    // 一旦结果可用,我们就可以使用该结果进行后续处理。
    std::cout <<"result " << result.get() << std::endl; // 在主线程等待 promise的返回 result set string


    return 0;
}
main1 -------------
do_some_other_things
print1 sleep
wait get result
result set string

二、function和bind用法

std::function std::bind 都是 C++ 的函数工具,用于实现函数对象的封装和绑定。

2.1 function

std::function 是一个通用的函数封装类模板,用于存储、管理和调用可调用对象(如函数、函数指针、Lambda 表达式等)。它提供了一种统一的方式来处理不同类型的可调用对象,并且可以像普通函数一样进行调用。类似C语言的函数指针。

包含头文件:#include

1)保存普通函数

void func1(int a)
{
  cout << a << endl;
}

std::function<void(int a)> func;
func = func1;
func(2);  //2

2)保存lambda表达式

std::function<void()> func_1 = [](){cout << "hello world" << endl;};  
func_1();  //hello world

3)保存成员函数

class A{
public:
  A(string name) : name_(name){}
  void func3(int i) const {cout <<name_ << ", " << i << endl;}
private:
  string name_;
};
//3 保存成员函数
std::function<void(const A&,int)> func3_ = &A::func3;
A a("darren");
func3_(a, 1);

2.2 bind

用于将可调用对象与其参数进行绑定,生成一个新的可调用对象。

调用bind的一般形式:

auto newCallable = bind(callable, arg_list);

其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。即,当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。

arg_list中的参数可能包含形如n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:1为newCallable的第一个参数,_2为第二个参数,以此类推。

范例

#include 
#include 
using namespace std;
class A
{
public:
    // 重载fun_3,主要bind的时候需要
//    std::bind((void(A::*)(int, int))&A::fun_3
    void fun_3(int k,int m)
    {
        cout << "fun_3 a = " << a<< endl;
        cout<<"print: k="<<k<<",m="<<m<<endl;
    }
//    std::bind((void(A::*)(string))&A::fun_3
    void fun_3(string str) {
        cout<<"print: str="<<str<<endl;
    }
    int a;
};

void fun_1(int x,int y,int z)
{
    cout<<"fun_1 print: x=" <<x<<",y="<< y << ",z=" <<z<<endl;
}

void fun_2(int &a,int &b)
{
    a++;
    b++;
    cout<<"print: a=" <<a<<",b="<<b<<endl;
}

void func2_1(int a, int b)
{
    cout << "func2_1 a + b = " << a+b << endl;
}

int func2_1(string a, string b)
{
    cout << "func2_1 a + b = " << a << b<< endl;
    return 0;
}

1)先看绑定函数

    //f1的类型为 function
    cout << "\n\nstd::bind(fun_1, 1, 2, 3) -----------------\n";
    auto f1 = std::bind(fun_1, 1, 2, 3); //表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3
    f1(); //print: x=1,y=2,z=3

    cout << "\n\nstd::bind(fun_1, 10, 20, 30) -----------------\n";
    auto f11 = std::bind(fun_1, 10, 20, 30); //表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3
    f11();


    cout << "\n\nstd::bind(fun_1, placeholders::_1,placeholders::_2, 3) -----------------\n";
    auto f2 = std::bind(fun_1, placeholders::_1, placeholders::_2, 3);
    //表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f2 的第一,二个参数指定
    f2(1,2);//print: x=1,y=2,z=3
    f2(10,21,30); // 传入30也没有用


   cout << "\n\nstd::bind(fun_1,placeholders::_2,placeholders::_1,3) -----------------\n";
   auto f3 = std::bind(fun_1,placeholders::_2,placeholders::_1,3);
   //表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f3 的第二,一个参数指定
   //注意: f2  和  f3 的区别。
   f3(1,2);//print: x=2,y=1,z=3

   cout << "\n\nstd::bind(fun_2, placeholders::_1, n) -----------------\n";
   int m = 2;
   int n = 3;
   auto f4 = std::bind(fun_2, placeholders::_1, n); //表示绑定fun_2的第一个参数为n, fun_2的第二个参数由调用f4的第一个参数(_1)指定。
   f4(m); //print: a=3,b=4
   cout<<"m="<<m<<endl;//m=3  说明:bind对于不事先绑定的参数,通过std::placeholders传递的参数是通过引用传递的,如m
   cout<<"n="<<n<<endl;//n=3  说明:bind对于预先绑定的函数参数是通过值传递的,如n

std::bind(fun_1, 1, 2, 3) -----------------
fun_1 print: x=1,y=2,z=3


std::bind(fun_1, 10, 20, 30) -----------------
fun_1 print: x=10,y=20,z=30


std::bind(fun_1, placeholders::_1,placeholders::_2, 3) -----------------
fun_1 print: x=1,y=2,z=3
fun_1 print: x=10,y=21,z=3


std::bind(fun_1,placeholders::_2,placeholders::_1,3) -----------------
fun_1 print: x=2,y=1,z=3


std::bind(fun_2, placeholders::_1, n) -----------------
print: a=3,b=4
m=3
n=3

2)再看一下绑定类

    cout << "\n\nstd::bind(&A::fun_3, &a,placeholders::_1,placeholders::_2) -----------------\n";
    A a;
    a.a = 10;
    //f5的类型为 function, 使用auto关键字
    auto f5 = std::bind((void(A::*)(int, int))&A::fun_3, &a, 40, 50); //void是返回类型,(A::*) 是指向类 A 的成员的作用域限定符,(int, int) 是函数参数列表
    f5(10,20);

    cout << "\n\nstd::bind(&A::fun_3, &a2,placeholders::_1,placeholders::_2) -----------------\n";
    A a2;
    a2.a = 20;
    //f5的类型为 function
    auto f6 = std::bind((void(A::*)(int, int))&A::fun_3, &a2,placeholders::_1,placeholders::_2); //使用auto关键字
    f6(10,20);//


    cout << "\n\nstd::bind(&A::fun_3, a,std::placeholders::_1,std::placeholders::_2) -----------------\n";
    auto f_str = std::bind((void(A::*)(string))&A::fun_3, a,std::placeholders::_1);
    f_str("darren");
std::bind(&A::fun_3, &a,placeholders::_1,placeholders::_2) -----------------
fun_3 a = 10
print: k=40,m=50


std::bind(&A::fun_3, &a2,placeholders::_1,placeholders::_2) -----------------
fun_3 a = 20
print: k=10,m=20


std::bind(&A::fun_3, a,std::placeholders::_1,std::placeholders::_2) -----------------
print: str=darren

说明的一点是,如果没有重载的话,可以直接写

auto f5 = std::bind(&A::fun_3, &a, 40, 50);

三、可变参数模板

3.1 语法

可变参数模板是 C++11 提供的一种特性,用于处理数量可变的参数类型。通过可变参数模板,可以在一个模板函数或类中接受任意数量和类型的参数。

template<class... T, class... Args>
void functionName(T first, Args... args) {
    // 函数体
}

1)使用 template 关键字定义一个模板函数,其模板类型参数由 class... T表示。T 是第一个参数的类型,而 Args 是剩余参数的类型包(parameter pack)。
2)第一个参数 T first 是必传的,用来接收第一个参数。
3)Args... args 是一个参数包扩展(pack expansion),用于接收剩余的参数。通过 … 表示参数包的展开,可以将其中的每个参数依次进行处理。

例如

#include 

using namespace std;

template <class... T>
void fun(T... args)
{
    cout << sizeof...(args) << endl; //打印变参的个数
}
int main()
{    
    fun();        //0
    fun(1, 2);    //2
    fun(1, 2.5, "");    //3
    return 0;
}

这个例子只是简单的将可变模版参数的个数打印出来,如果我们需要将参数包中的每个参数打印出来的话就需要通过一些方法了。展开可变模版参数函数的方法一般有三种:
1)通过递归函数来展开参数包,
2)是通过逗号表达式来展开参数包。
3)c++ 17的折叠表达式

3.2 递归函数来展开参数包

通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数,递归终止函数正是用来终止递归的

#include 

using namespace std;

//递归终止函数
void print()
{
   cout << "empty" << endl;
}

//展开函数
template <class T, class ...Args>
void print(T head, Args... rest)
{
   cout << "parameter " << head << endl;
   print(rest...);
}


int main(void)
{
   print(1,2,3,"zxm", "xxjp");
   return 0;
}
parameter 1
parameter 2
parameter 3
parameter zxm
parameter xxjp
empty

上例会输出每一个参数,直到为空时输出empty。展开参数包的函数有两个,一个是递归函数,另外一个是递归终止函数,参数包Args...在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当没有参数时,则调用非模板函数print终止递归过程。

上面的递归终止函数还可以写成这样:

template <class T>
void print(T t)
{
 cout << t << endl;
}

3.3 逗号表达式展开参数包

递归函数展开参数包是一种标准做法,也比较好理解,但就是必须要一个重载的递归终止函数,即必须要有一个同名的终止函数来终止递归。

逗号表达式是 C++ 中的一种表达式,由逗号(,)操作符连接多个子表达式而形成。逗号表达式的求值顺序是从左到右,每个子表达式都会被依次求值,最终整个逗号表达式的结果是最后一个子表达式的值。

int a = 1, b = 2, c;
c = (a++, b++, a + b);  // 逗号表达式 (a++, b++) 的结果是 b,c 的值为 a + b 的结果

初始化列表(initializer list)是一种在 C++ 中用于初始化数组、结构体、类等复合类型对象的语法。它使用花括号 {} 将初始化的值括起来,多个值之间用逗号分隔。

int arr[] = {1, 2, 3, 4, 5};  // 初始化整型数组

逗号表达式可以不通过递归方式来展开参数包,这种方式需要借助逗号表达式和初始化列表。比如前面print的例子可以改成这样:

#include 

using namespace std;

template <class T>
void printarg(T t)
{
    cout << t << endl;
}

template <class ...Args>
void expand(Args... args)
{
    int arr[] = {(printarg(args), 0)...};
}

int main()
{
    expand(1,2,3,"zxm");
    return 0;
}

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。

expand函数中的逗号表达式:(printarg(args), 0),先执行printarg(args),再得到逗号表达式的结果0。
同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组,{(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]

由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

我们可以把上面的例子再进一步改进一下,将函数作为参数,就可以支持lambda表达式了,从而可以少写一个递归终止函数了,具体代码如下:

#include 
using namespace std;
template<class F, class... Args>void expand(const F& f, Args&&...args)
{
 //这里用到了完美转发
 initializer_list<int>{(f(std::forward< Args>(args)),0)...};
}
int main()
{
  expand([](int i){cout<<i<<endl;}, 1,2,3);
  return 0;
}

3.4 折叠表达式

#include 

template<typename... Args>
void printArgs(Args... args) {
    ((std::cout << args << std::endl), ...);  // 折叠表达式展开参数包并换行
//    ((std::cout << args << ' '), ...);  // 折叠表达式展开参数包并添加空格
}

int main() {
    printArgs(1, 2.5, "hello", 'a');
    return 0;
}

需要注意的是,编译要加上-std = c++17

本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接,详细查看详细的服务器课程

你可能感兴趣的:(C/C++,c++)