前言:
本文介绍的是包装器以及线程库的简单了解,但是呢,线程是基于对Linux有一定的了解,所以本文就是简单介绍一下,介绍完包装器以及线程库的简单理解之后C++11的特性就到此为止,当然C++11远不止于此!
1 包装器
先来了解包装器的大体分类,分为两种,一种是类模板包装器,一种是函数模板包装器,说是两大类,今天介绍两个,一个是function包装器,一个是bind包装器。
1.1 function
在学习function之前,我们先来了解一下什么是可调用对象?
可调用对象就是指可以实例化的并且可以调用的对象呗:
仿函数是吧?但是缺点是要实例化出多个类,并且类型不太好控制,比如面对自定义类型的操作。
函数指针是吧?但是C++不太喜欢使用。
lambda表达式是吧?但是lambda表达式没有类型概念,所以每次需要auto接受,相对于其他两个来说,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;
}
使用了三种不一样的可调用对象,乍一看好像没有问题,但是运行的时候可以发现useF函数实例化出了三份,这在模板学习的时候就提及了,是编译器在负重前行,那么现在我们希望给编译器减少一点负担,即实例化出一个就行,预期结果是打印出来的count的地址都是一样的,并且个数为1.
此时可以使用function了,function的头文件位于functional中,定义如下:
看着是有点看不懂吧,咱们直接使用一下:
struct Functor
{
int operator()(const int& a, const int& b)
{
return a + b;
}
};
int f(const int& a, const int& b)
{
return a + b;
}
int main()
{
function<int(int, int)> f1 = Functor();
function<int(int, int)> f2 = f;
function<int(int, int)> f3 = [](const int a, const int b) {return a + b; };
f1(1, 2);
f2(3, 4);
f3(5, 4);
return 0;
}
语法看起来有点怪的,最外层的尖括号里面是返回值和参数,圆括号括起来的是函数参数,外面的是函数返回值,注意,这里需要保持基本类型一致,比如前面写f2 = f,f函数的基本数据类型是Int,所以说包装器function的基本类型也是三个int。
那么怎么使用function解决类模板实例化多份的问题呢?
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()
{
function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
function<double(double)> func3 = [](double d)->double
{
return d /4;
};
cout << useF(func3, 11.11) << endl;
return 0;
}
简单呀,包装一下就可以了,但是为什么经过包装了之后就可以完成只实例化一个对象呢?
前面实例化多份对象的原因是因为模板参数接受的参数不一样,函数指针啊 匿名对象啊 lambda表达式啊,但是最终完成的操作都是实现除,所以就实例化了多份对象。
经过function包装了之后,参数都是一样的,就不存在说一定要经过函数模板再去调用了,直接就调用了。
以上是function的基本用法,那么现在进阶一下。
上面的包装都是包装一整个类或者是函数对象等,如果一个类有多个成员函数,我们只想要包装其中一个成员函数呢?
这里就要分为静态成员函数和非静态成员函数了。
首先来看一下静态成员函数的包装:
class Master
{
public:
static int Mass(int a, int b)
{
return a + b;
}
int Masn(int a,int b)
{
return a + b;
}
};
int main()
{
//&符号可以加可以不加
function<int(int,int)> f1 = &Master::Mass;
f1(2,2);
return 0;
}
因为是静态函数,所以需要类域来访问,这里的语法就是这样的,对于&符号来说加不加都是可以的,重点在于非静态成员函数:
class Master
{
public:
static int Mass(int a, int b)
{
return a + b;
}
int Masn(int a,int b)
{
return a + b;
}
};
int main()
{
//&符号可以加可以不加
function<int(int,int)> f1 = &Master::Mass;
f1(2,2);
//&符号必须加
function<int(Master*,int, int)> f2 = &Master::Masn;
Master m1;
f2(&m1, 1, 2);
function<int(Master,int,int)> f3 = &Master::Masn;
f3(Master(),1, 1);
return 0;
}
先看语法使用,语法使用如上。
首先,取地址符号是一定要加的,其次,域名访问限定符也是要加的,那么为什么要传类类型的指针或匿名对象呢?
思考一个问题,非静态成员函数的参数有多少个?这里要特别注意的是,除了显式的两个int,还有this指针!所以,为什么保持参数一致,我们就应该传类类型的指针。
但是为什么传匿名对象也可以呢?思考一个问题,参数传给的是funtion吗?参数是传给类对象的,然后通过类对象调用函数,funtion只是起到了一个包装的作用,实际上的参数调用还是要通过类对象来实现,那么函数由谁调用,由函数指针,或者是函数对象调用,所以这里包装非静态成员函数的实质还是要通过对象来调用函数,所以两种传值的方式都是可以的。
1.2 bind
bind属于funtional里面的Functions部分,function属于classes部分,这也说明了它们一个是类模板包装器,一个是函数模板包装器。
那么bind的作用是什么呢?
bind一般有两个作用,一个是调整参数的顺序,一个是调整参数的个数,其中调整顺序一般不太用,毕竟用处没那么大,调整参数的个数是很有用的。
但是要注意,这里的调整参数个数不是删除某个参数或者是添加参数什么的,这样干的话已经破坏了函数本身了,这里的调整参数,比如让参数从4个到3个的意思是将其中的一个参数固定,就像缺省值那样,你不用传值,函数调用的时候已经固定了要传这个值,这是调整参数个数的本质。
调用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为第二个参数,以此类推。
而_1 _2所在的命名空间是在placeholders里面,该命名空间同样也在functional里面,对应的1和2就是参数的顺序。
int Sub(int a, int b)
{
return a - b;
}
int main()
{
//正常调用
auto f1 = Sub;
f1(1, 2);
//未调整顺序 -> bind
auto f2 = bind(Sub, placeholders::_1, placeholders::_2);
cout << f2(10, 5) << endl;
//调整顺序 -> bind
auto f3 = bind(Sub, placeholders::_2, placeholders::_1);
cout << f3(10, 5) << endl;
return 0;
}
根据bind的参数,第一个参数是可调用对象,后面的是参数列表,所以第一个参数传的就是可调用对象,调整参数顺序就是调整_1 _2的位置,可以理解为_2 代表的是第二个参数,所以_1 _2交换位置就是调整参数的顺序了,这里呢,多参数也是一样的,都可以进行相应的顺序调整。
这里需要注意的是,如果是对一个类的话,是不能传一整个类的,只能传类的某个函数。
接下来是参数个数的调整,说白了,就是固定参数:
class Sub
{
public:
Sub(int x)
:_x(x)
{}
int sub(int a, int b)
{
return (a - b) * _x;
}
private:
int _x;
};
int main()
{
auto f2 = bind(&Sub::sub, placeholders::_1, placeholders::_2,placeholders::_3);
cout << f2(Sub(1), 10, 5) << endl;
Sub sub(1);
cout << f2(&sub, 10, 5) << endl;
auto f3 = bind(&Sub::sub, &sub, placeholders::_1, placeholders::_2);//参数的顺序是相对的
cout << f3(10, 5) << endl;
return 0;
}
对于类来说,bind的时候,语法规定需要域名访问限定符和取地址符号,那么调用的时候,同function一样,选择指针调用或者是对象调用,所以可以传匿名对象,也可以传地址过去。
f3就是一种固定参数,因为成员函数的参数列表第一个就是this指针,所以第一个参数固定的话就是sub的地址,那么为什么后面是_1 _2呢?因为第一个参数固定了,相当于只有两个参数了,所以说是相对的。
对于固定参数来说,可以选择固定任意的,比如可以固定第二个参数,其他参数不固定的话顺序和上面的一样的。
至此,引入一个让人意想不到的事实:
包装器的底层,同样是仿函数:
00007FF6CB3BF0C1 call std::_Binder<std::_Unforced,int (__cdecl Sub::*)(int,int) __ptr64,std::_Ph<1> const & __ptr64,std::_Ph<2> const & __ptr64,std::_Ph<3> const & __ptr64>::operator()<Sub,int,int,0> (07FF6CB3B191Fh)
第一个是function的底层,第二个是bind的底层,不太好截图。
很神秘吧!
感谢阅读!->因为作者对于线程的理解确实不够,所以有关线程的介绍留在后面,嘿嘿。