本章是讲解学习C++11语法新特性的第三篇文章,主要学习lambda表达式,包装器,线程库
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法
#include <algorithm>
#include <functional>
int main()
{
int array[] = { 4,1,8,5,3,7,0,9,2,6 };
// 默认按照小于比较,排出来结果是升序
std::sort(array, array + sizeof(array) / sizeof(array[0]));
for (int i = 0; i < 10; i++)
{
cout << array[i] << " ";
}cout << endl;
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
for (int i = 0; i < 10; i++)
{
cout << array[i] << " ";
}cout << endl;
return 0;
}
注:如果待排序元素为自定义类型,需要用户定义排序时的比较规则
struct Goods
{
string _name;
double _price;
};
struct Compare
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price <= gr._price;
}
};
int main()
{
Goods gds[] = { { "苹果", 2.1 }, { "香蕉", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), Compare());
for (int i = 0; i < 4; i++)
cout << gds[i]._name << ":"<<gds[i]._price<<" ";
cout << endl;
return 0;
}
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C11语法中出现了Lambda表达式
int main()
{
Goods gds[] = { { "苹果", 2.1 }, { "香蕉", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& l, const Goods& r)
->bool
{
return l._price < r._price;
});
return 0;
}
注:可以看出lamb表达式实际是一个匿名函数
[capture-list] (parameters) mutable -> return-type { statement }
捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用
参数列表,与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
默认情况下,lambda函数总是一个const函数, mutable的作用就是让传值捕捉的对象可以修改,但是你修改的是传值拷贝的对象,不影响外面对象,使用该修饰符时,参数列表不可省略(即使参数为空) 注:实际中mutable意义不大,除非你就是想传值捕捉过来,lambda中修改,不影响外面的值
返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略;返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
函数体,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量 注:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空,即C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[] {};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=]{ return a + 3; };
// 省略了返回值类型,无返回值类型
auto fun1 = [&](int c) { b = a + c; };
fun1(10);
cout << a << " " << b << endl;
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int {return b += a + c; };
cout << fun2(10) << endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a)mutable { x *= 2; return a + x; };//传值捕捉修改需要mutable修饰
auto add_x1 = [&x](int a){ x *= 2; return a + x; };//引用捕捉不用
cout << add_x(10) << endl;
cout << x << endl;
cout << add_x1(10) << endl;
cout << x << endl;
return 0;
}
注:lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
void (*PF)();
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将没有捕获任何变量的lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
Lambda是实现了函数调用运算符的匿名类(anonymous class)。对于每一个Lambda,编译器创建匿名类,并定义相应的数据成员存储Lambda捕获的变量。没有捕获变量的Lambda不包含任何含成员变量。一个没有任何成员变量(包括没有虚函数表指针)的类型,在空指针上调用成员函数也不会有任何的问题,因为它的成员函数不会通过this指针访问内存。当Lambda向函数指针的转换时,编译器为Lambda的匿名类实现函数指针类型转换运算符
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象
class Rate
{
public:
Rate(double rate): _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
r2(10000, 2);
return 0;
}
从使用方式上来看,函数对象与lambda表达式完全一样:函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到
注:实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的
function包装器也叫作适配器,C++中的function本质是一个类模板,也是一个包装器 由于C++的历史遗留问题,导致如果想实现一个函数功能,可以采用函数名、函数指针、仿函数、有名称的lambda表达式,所有这些都是可调用的类型
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
注:对于函数名称,仿函数对象,lambda表达式对象这些都是可调用的类型,我们发现发现useF函数模板实例化了三份,所以如此丰富的类型,可能会导致模板的效率低下,包装器可以很好的解决该问题
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
注:std::function在头文件< functional >
示例:
#include <functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lamber表达式
std::function<int(int, int)> func3 = [](const int a, const int b){
return a + b;
};
cout << func3(1, 2) << endl;
// 类的成员函数
std::function<int(int, int)> func4 = Plus::plusi;
cout << func4(1, 2) << endl;
std::function<double(Plus, double, double)> func5 = &Plus::plusd;//对于普通成员的包装一定要加上&,需要通过指针进行调用成员函数
cout << func5(Plus(), 1.1, 2.2) << endl;//传入类对象,通过对象进行调用
return 0;
}
#include <functional>
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
//将多个可调用类型进行封装成相同类型,便于统一调用
// 函数名
std::function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
// 函数对象
std::function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
// lamber表达式
std::function<double(double)> func3 = [](double d)->double {
return d /4;
};
cout << useF(func3, 11.11) << endl;
return 0;
}
#include <functional>
int Plus(int a, int b)
{
return a + b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
static int sub1(int a, int b)
{
return a - b;
}
};
int main()
{
//普通函数的绑定
//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定(placeholders用来表示参数位占位)
std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,placeholders::_2);
//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);直接使用auto识别类型
//表示绑定函数 plus 的第一,二参数为: 1, 2
auto func2 = std::bind(Plus, 1, 2);
cout << func1(1, 2) << endl;
cout << func2(2,3) << endl;//func2();也可以不用传参数-因为参数已经绑定好了,传入的参数没有实际的作用
//类函数的绑定
//类的成员函数必须通过类的对象或者指针调用,因此在bind时,bind的第一个参数的位置来指定一个类的实列、指针或引用。
Sub s;
// 绑定成员函数
std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,placeholders::_1, placeholders::_2);
// 参数调换顺序
std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,placeholders::_2, placeholders::_1);
std::function<int(int, int)> func5 = std::bind(Sub::sub1,placeholders::_2, placeholders::_1);//静态成员函数的绑定-不需要类的示例指针或引用
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
cout << func5(1, 2) << endl;
return 0;
}
bind是对包装的可调用类型的进一步封装,可以根据自己的需要进行调整参数的数据及位置,绑定类对象能有优化成员函数的包装使用,更加符合使用习惯
注:要使用标准库中的线程,必须包含< thread >头文件
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn, args1, args2, …) | 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的 参数 |
get_id() | 获取线程id |
jionable() | 线程是否还在执行,joinable代表的是一个正在执行中的线程。 |
jion() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离 的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
#include <thread>
int main()
{
std::thread t1;//空线程
cout << t1.get_id() << endl;
return 0;
}
注:get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体
// vs下查看
typedef struct
{ /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;
#include <iostream>
#include <thread>
using namespace std;
void ThreadFunc(int a)
{
cout << "Thread1" << a << endl;
}
class TF
{
public:
void operator()()
{
cout << "Thread3" << endl;
}
};
int main()
{
// 线程函数为函数指针
thread t1(ThreadFunc, 10);
// 线程函数为lambda表达式
thread t2([]() {
cout << "Thread2" << endl;
});
// 线程函数为函数对象
TF tf;
thread t3(tf);
t1.join();
t2.join();
t3.join();
cout << "Main thread!" << endl;
return 0;
}
可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参
#include <iostream>
#include <thread>
using namespace std;
void Func1(int& x)
{
x += 10;
return;
}
void Func2(int* x)
{
*x += 10;
return;
}
int main()
{
int a = 10;
// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝
// vs2019会报错-对于引用的参数这么传入
//thread t1(Func1, a);
//t1.join();
//cout << a << endl;
// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
thread t2(Func1, ref(a));
t2.join();
cout << a << endl;
// 地址的拷贝
thread t3(Func2, &a);
t3.join();
cout << a << endl;
return 0;
}
如果是类成员函数作为线程参数时,必须将this作为线程函数参数
#include <iostream>
#include <thread>
using namespace std;
class A
{
public:
void Func1(int x)
{
cout << x << endl;
}
static void Func2(int x)
{
cout << x << endl;
}
};
int main()
{
A a;
//普通成员函数需要传入类的实例或者指针
thread t1(&A::Func1, a, 10);
t1.join();
//静态成员函数则不用
thread t2(&A::Func2, 10);
t2.join();
return 0;
}
多线程最主要的问题是共享数据带来的问题(即线程安全):如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据;但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦
#include <iostream>
#include <thread>
using namespace std;
unsigned long sum = 0L;
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
sum++;
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}
C++98中传统的解决方式:可以对共享修改的数据可以加锁保护
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
std::mutex m;
unsigned long sum = 0L;
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
{
m.lock();
sum++;
m.unlock();
}
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}
只要一个线程在sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁
因此C++11中引入了原子操作,所谓原子操作:即不可被中断的一个或一系列操作C++11引入的原子操作类型,使得线程间数据的同步变得非常高效
注:需要使用以上原子操作变量时,必须添加头文件#include < atomic >
#include <iostream>
#include <thread>
using namespace std;
#include <atomic>
atomic_long sum{ 0 };
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
sum++; // 原子操作
}
int main()
{
cout << "Before joining, sum = " << sum << std::endl;
thread t1(fun, 1000000);
thread t2(fun, 1000000);
t1.join();
t2.join();
cout << "After joining, sum = " << sum << std::endl;
return 0;
}
int main()
{
thread t1(fun, 1000000);
thread t2(fun, 1000000);
t1.join();
t2.join();
//printf("%d\n", sum);vs2019存在类型不匹配问题
//解决方式
//1.
printf("%ld\n", sum.load());
//2.
cout << sum << endl;
//3.
printf("%ld\n", (long)sum);
return 0;
}
在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问,更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型
atmoic<T> t; // 声明一个类型为T的原子类型变量t
原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了
#include <atomic>
int main()
{
atomic<int> a1(0);
//atomic<int> a2(a1); // 编译失败
atomic<int> a2(0);
//a2 = a1; // 编译失败
return 0;
}
C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动
函数名 | 函数功能 |
---|---|
lock() | 上锁:锁住互斥量 |
unlock() | 解锁:释放对互斥量的所有权 |
try_lock() | 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞 |
recursive_mutex和timed_mutex的结合
std::lock_gurad 是 C++11 中定义的模板类。
定义如下:
template<class _Mutex>
class lock_guard
{
public:
// 在构造lock_gard时上锁
explicit lock_guard(_Mutex& _Mtx)
: _MyMutex(_Mtx)
{
_MyMutex.lock();
}
lock_guard(_Mutex & _Mtx, adopt_lock_t)
: _MyMutex(_Mtx)
{}
// 在析构lock_gard时解锁
~lock_guard() _NOEXCEPT
{
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题
太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int main()
{
int i = 1;
int j = 2;
bool flg = true;
mutex mtx;
condition_variable cv;
//存在时间片切出去的问题
thread t2([&j, &mtx, &cv]() {
while (j <= 100)
{
std::unique_lock<mutex> lock(mtx);
cv.wait(lock);
cout << std::this_thread::get_id() << ":" << j << endl;
j += 2;
cv.notify_one();
}
});
thread t1([&i, &mtx, &cv]() {
while (i <= 100)
{
std::unique_lock<mutex> lock(mtx);
cout << std::this_thread::get_id() << ":" << i << endl;
i += 2;
cv.notify_one();
cv.wait(lock);
}
});
t1.join();
t2.join();
return 0;
}
当打印偶数线程获取锁后,在要等待在条件变量下之前时,时间片到了线程被切出去,再等到打印奇数线程执行唤醒等待条件变量下的线程时没有线程被唤醒,当打印偶数线程时间片切回时,依旧会等待在条件变量下,而此时打印奇数线程也等待在条件变量下,此时没人进行唤醒两线程也就会一直进行等待
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
int main()
{
int i = 1;
int j = 2;
bool flg = true;
mutex mtx;
condition_variable cv;
//正确写法
thread t1([&i, &mtx, &cv,&flg]() {
while (i <= 100)
{
std::unique_lock<mutex> lock(mtx);
cv.wait(lock, [&flg]() { return flg; });//根据条件判断是否需要进行阻塞等待
cout << std::this_thread::get_id() << ":" << i << endl;
i+=2;
flg = false;//更改条件变量-使得另一个线程执行,该线程会等待住
cv.notify_one();//进行唤醒等待条件变量下的线程
}
});
thread t2([&j, &mtx, &cv, &flg]() {
while (j <= 100)
{
std::unique_lock<mutex> lock(mtx);
cv.wait(lock, [&flg]() { return !flg; });
cout << std::this_thread::get_id() << ":" << j << endl;
j += 2;
flg = true;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}
确示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
int main()
{
int i = 1;
int j = 2;
bool flg = true;
mutex mtx;
condition_variable cv;
//正确写法
thread t1([&i, &mtx, &cv,&flg]() {
while (i <= 100)
{
std::unique_lock<mutex> lock(mtx);
cv.wait(lock, [&flg]() { return flg; });//根据条件判断是否需要进行阻塞等待
cout << std::this_thread::get_id() << ":" << i << endl;
i+=2;
flg = false;//更改条件变量-使得另一个线程执行,该线程会等待住
cv.notify_one();//进行唤醒等待条件变量下的线程
}
});
thread t2([&j, &mtx, &cv, &flg]() {
while (j <= 100)
{
std::unique_lock<mutex> lock(mtx);
cv.wait(lock, [&flg]() { return !flg; });
cout << std::this_thread::get_id() << ":" << j << endl;
j += 2;
flg = true;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}