在公司说的项目代码中看到了std::function、std::bind、std::placeholders三个C++11的特性,通过了解之后,发现还是挺有用的,在这里记录下吧。似乎这三个特性一般都是一起使用的,所以也一起讲了。
三个特性都在functional文件中定义,故使用时需要“#include<functional>”
1. 基础介绍
这里先简单介绍下三个特性。
1.1 std::function
template< class >
class function; /* undefined */
template< class R, class... Args >
class function<R(Args...)>;
官方说明是:“Class template std::function is a general-purpose polymorphic function wrapper. Instances of std::function can store, copy, and invoke any Callable target -- functions, lambda expressions, bind expressions, or other function objects, as well as pointers to member functions and pointers to data members.”
即它是一个通用的多态函数封装器,它的实例可以存储、赋值以及调用任何可以调用的目标:函数,lambda表达式、bind表达式或其他的函数对象,还有指向成员函数指针和指向数据成员指针。
被存储的可调用对象即是std::fucntion的目标,通过赋值方式将目标存储到std::function后,就可以通过调用std::function对象来间接调用被存储的对象了,一般被存储的对象都是函数。如果std::function没有包含目标,则是空目标,调用这样子的std::function将抛出std::bad_function_call异常。
实际上用于存储的std::function也是一个对象,在存储的时候需要指明存储目标的返回值及参数等。
1.2 std::bind
template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );
template< class R, class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );
官方说明是:“The function template bind generates a forwarding call wrapper for f. Calling this wrapper is equivalent to invoking f with some of its arguments bound to args.”
std::bind是一个函数模板,就像是一个函数适配器,接受一个可调用对象,而生成一个新的可调用对象来适配原来的参数列表,该模板返回的是一个std::function对象。用该函数我们也能实现参数顺序的调整和给将指定参数设置成固定值。
1.3 std::placeholders
占位符,通过查看functional文件可以看到c++11中有29个占位符,分别是_1~_29,一般情况下是写std::placeholders::_1这样子的。占位符的作用就是用来代表参数的,std::placeholders::_1表示的是std::bind得到的std::function对象被调用时,传入的第一个参数,而std::placeholders::_2则是第二个参数。
我们调整std::placeholders::_x在std::bind时的顺序,就可以起到调整参数顺序的作用了。此外,我们也可以在std::bind的时候不用std::placeholders::_x,而直接写成固定的值,这样子调用std::function存储的对象时,对应位置的参数将是固定值。
2. 例子
下面的代码是使用的例子,注意编译的时候带上“-std=c++11”,否则是无法编译过的。
#include <functional>
#include <iostream>
int g_Minus(int i, int j)
{
std::cout << "in g_Minus: " << i << " " << j << std::endl;
return i - j;
}
class tClass {
public:
void setID(int id) {
id_ = id;
}
int getID() {
return id_;
}
static int wrongID() {
return 100;
}
int id_ = 5;
private:
void psetID(int id) {
id_ = id;
}
};
struct Add
{
int operator()(int i, int j) {
return i + j;
}
};
template <class T>
struct Sub
{
T operator()(T i, T j) {
return i - j;
}
};
template <class T>
T Mul(T i, T j) {
return i * j;
}
int main()
{
// 例子1. 普通函数
std::function<int(int,int)> f = g_Minus;
int ret = f(1,2);
std::cout << ret << std::endl;
// 例子2. 类的静态成员函数
std::function<int(void)> f2 = &tClass::wrongID;
ret = f2();
// 例子3. 类普通成员函数
tClass c1;
std::function<void(int)> f3 = std::bind(&tClass::setID, &c1, std::placeholders::_1);
std::cout << c1.getID() << std::endl;
f3(10);
std::cout << c1.getID() << std::endl;
// 例子4. 函数对象
std::function<int(int,int)> f4 = Add(); // 绑定到了一个函数对象,Add()是会返回一个匿名的函数对象
std::cout << f4(5, 8) << std::endl;
// 例子5. 模板函数对象
std::function<float(float, float)> f5 = Sub<float>();
std::cout << f5(11.55, 3.33) << std::endl;
// 例子6. 模板函数
std::function<double(double,double)> f6 = Mul<double>;
std::cout << f6(11.55, 3.33) << std::endl;
// 例子7. 类普通成员函数
std::function<void(tClass &, int)> f9 = &tClass::setID;
f9(c1, 150);
std::cout << c1.getID() << std::endl;
// 例子8. 适配成一个参数,并且将第二个参数设置成固定值5
std::function<int(int)> f8 = std::bind(g_Minus, std::placeholders::_1, 5);
std::cout << f8(6) << std::endl;
// 例子9. 调整参数顺序
std::function<int(int, int)> f10 = std::bind(g_Minus, std::placeholders::_2, std::placeholders::_1);
std::cout << f10(8, 3) << std::endl;
// 例子10. 类成员
std::function<int(tClass const&)> n1 = &tClass::id_;
std::cout << n1(c1) << std::endl;
return 1;
}
结合上面的说明,基本上可以了解到存储各种目标到std::function的用法了,这里没有举例用于lambda表达式的情况,因为我C++11的lambda表达式还不熟。
另外,从上面的例子3和例子7我们可以发现对于类普通成员的存储有两种方式,一种是用到std::bind,一种是没有用到的,导致实际调用std::function对象的时候也不一样。但是实际上的调用过程还是一样的,只不过std::bind起到了适配器的作用,已经将setID的第一个参数默认为c1的地址了,而不用bind的则需要我们传入tClass对象。那为什么可以这样子做呢?实际上我们通过调用类的成员函数的时候,编译出来的汇编代码是默认第一个参数为类对象的this指针的,第二个参数开始才是我们传入的参数,所以我们在这里的调用方式,才是更接近底层一点的调用方式的。
从上面的例子3、8、9我们可以了解到占位符std::placeholder的作用。
3. 为什么要用std::function
一开始看到std::function的用法的时候,想到的就是函数指针,为什么有了指针,还要弄出个std::function呢?后面通过查找资料了解到,主要有两点:1. 函数指针是不安全的,在通过函数指针调用的时候,并不会进行参数检查,也不会检查返回值。因为是通过强制类型转换的吧,这样子就很容易出问题;2. 函数指针无法绑定到类的成员函数上,编译的时候会报错。最主要的还是第一点吧!
#include <iostream>
void test(int param)
{
int ret = 0;
for (int i = 0; i < param; ++i) {
ret += i;
}
ret >>= 16;
ret |= (ret << 16);
}
typedef bool (*PFUNC)(char args, int arg2);
int main(void)
{
PFUNC func;
func = (PFUNC)test;
if (func('a', 111)) {
std::cout << "good" << std::endl;
}
else {
std::cout << "bad" << std::endl;
}
return 0;
}
上面的函数中,test只需要一个参数而没有返回值,但是我可以把它的地址强行赋给一个需要两个参数并且返回布尔类型的函数指针,这样子很容易出问题。至于无法绑定到类的成员函数上,我测试了编译不通过,所以也没有代码了。
通过上面的例子,我们很容易发现函数指针存在的问题,而std::function由于是类模板,所以在参数传递的时候会进行参数及返回值的检查,这样子就比函数指针安全很多了。
4. 应用例子
而由于std::function可以绑定到类的成员函数上,我们可以使用同一个接口,而在同一类中同时调用多个其他类的成员函数了。在所在公司中,是使用这个特性实现了事件的注册和处理机制。部分代码如下:
typedef std::function<int (R&, const E&)> CbFuncType;
class EMgr {
public:
void Register(uint32_t event_type, CbFuncType func);
void Raise(Role& role, Event event);
private:
std::map<uint32_t, std::vector<CbFuncType> > event_hendle_map_;
};
typedef Singleton<EMgr> EMgrSingleton;
#define REGISTER(event, func) \
EMgrSingleton::get_mutable_instance().Register(event, \
std::bind(&func, this, std::placeholders::_1, std::placeholders::_2));
所有需要注册的类中同一事件的执行函数的声明都是一样的。注册的时候通过std::bind生成一个匿名对象与对应的事件id绑定到一起,添加到事件map中对应的vector中去。注意这里也需要将注册的类的对象传进去,上面的this在各个类使用REGISTER宏的时候就会替换为对应的类对象了。
事件触发的时候,只需要遍历对应事件id的vector,并逐个执行std::function对象就行了。这里很方便的就把所有类中事件的执行函数(是成员函数)保存起来了。
使用非常方便,真的是一个很不错的特性。