在C++98中就有了函数对象(function object),为了方便从二元函数得到一元函数,引入了bind1st和bind2nd,这种场景在C++ STL中应用的很常见。随后C++11从boost库中引入了功能更强大的bind (以替代bind1st和bind2nd) 以及function函数对象机制。由于函数对象仍然需要额外定义,且有时很不方便,所以又引入了lambda表达式。
本文逐一介绍这些概念,以及背后的实现原理。
函数和函数对象
函数对象在C++98开始就已经被标准化。它的定义是:把有operator()小括号运算符重载函数的对象称之为函数对象(function object),或者成为仿函数(functor)。比如,如果要将两个对象相加,可以编写如下函数对象:
class Sum
{
public:
int operator()(int lhs, int rhs)
{
return lhs + rhs;
}
};
不过函数对象一般定义为struct,这样就可以省略写public,比如:
struct Sum{
int operator()(int lhs, int rhs)
{
return lhs + rhs;
}
};
使用方法如下:
Sum sum;
std::cout << sum(10, 20) << endl;
调用sum(10,20),实际上调用的是sum.operator()(10,20),输出结果为:30.
看起来也很简单,那么为什么在C中已经有函数,C++还要引入函数对象呢?比如,上述可以简单的使用函数实现:
int sum(int lhs, int rhs)
{
return lhs + rhs;
}
使用方法为直接调用:
std::cout << sum(10, 20) << endl;
这两个的区别为,一个是函数对象是一个对象,而函数仅仅是一个方法。这么说可能不明显,下面来举个例子:
假设有一个compare模板方法,用来比较两个对象的大小,并且允许用户自定义比较逻辑。可以分别使用函数和函数对象来实现,compare模板方法定义如下:
template <typename T, typename Compare>
bool compare(T lhs, T rhs, Compare comp)
{
return comp(lhs, rhs);
}
要比较大于或者小于,可以分别定义两个函数模板:
template <typename T>
bool mygreater(T lhs, T rhs)
{
return lhs > rhs;
}
template <typename T>
bool myless(T lhs, T rhs)
{
return lhs < rhs;
}
使用方法如下,比如要比较10和20的大小,可以如下调用:
std::cout << compare<int>(10, 20, mygreater<int>) << std::endl;
std::cout << compare<int>(10, 20, myless<int>) << std::endl;
输出结果为:
0
1
现在,如果使用函数对象,则需要定义两个类:
template <typename T>
class mygreater
{
public:
bool operator()(T lhs, T rhs)
{
return lhs > rhs;
}
};
template <typename T>
class myless
{
public:
bool operator()(T lhs, T rhs)
{
return lhs < rhs;
}
};
使用方法如下:
std::cout << compare<int>(10, 20, mygreater<int>()) << std::endl;
std::cout << compare<int>(10, 20, myless<int>()) << std::endl;
这两种方式的区别在于:
- 使用函数是无法内联的,在编译阶段,编译器无法确定该调用哪个函数,因为传递给模板方法的是一个函数指针。而使用函数对象,则在编译阶段,就能确定函数的调用方法,这样能去除函数调用的开销,从而提升应用程序效率。这应该就是C++中引入函数对象的主要原因。
- 函数对象是由类生成的,所以还可以添加相关的成员变量,用来记录函数对象使用时的更多信息。
在C++中有很多函数对象,比如greater,less等,比如在C++中的priority_queue 优先级队列,默认的是大根堆,其签名如下,_Compare默认是less,即值越大优先级越大:
template<typename _Tp, typename _Sequence = vector<_Tp>,
typename _Compare = less<typename _Sequence::value_type> >
class priority_queue
less的签名如下:
/// One of the @link comparison_functors comparison functors@endlink.
template <typename _Tp>
struct less : public binary_function<_Tp, _Tp, bool>
{
_GLIBCXX14_CONSTEXPR
bool
operator()(const _Tp &__x, const _Tp &__y) const
{
return __x < __y;
}
};
使用方法如下:
priority_queue<int> que1;
for (size_t i = 0; i < 10; i++)
{
que1.push(rand() % 100);
}
while (!que1.empty())
{
cout << que1.top() << " ";
que1.pop();
}
输出结果为:
78 69 67 64 62 58 41 34 24 0
如果要实现小根堆,则需要传入一个函数对象:
using MinHeap = priority_queue<int, vector<int>, greater<int>>;
MinHeap que2;
for (size_t i = 0; i < 10; i++)
{
que2.push(rand() % 100);
}
while (!que2.empty())
{
cout << que2.top() << " ";
que2.pop();
}
在上面的代码中,我们给第三个参数传递了一个greater<int>函数对象,它的签名如下:
/// One of the @link comparison_functors comparison functors@endlink.
template <typename _Tp>
struct greater : public binary_function<_Tp, _Tp, bool>
{
_GLIBCXX14_CONSTEXPR
bool
operator()(const _Tp &__x, const _Tp &__y) const
{
return __x > __y;
}
};
现在输出结果如下:
5 27 27 36 42 45 61 81 91 95
C++中的set也是类似,在其底部是一个红黑树,set的构造函数中也可以接受自定义的函数对象来比较元素的大小。
绑定器
C++的STL中提供了很多算法,这些算法大多数都能接受一个函数对象,然而有些算法需要二元函数对象,有些算法需要一元函数对象,那么如何在现有的二元函数对象上构建一个一元函数对象呢,这就是绑定器的作用。
bind1st和bind2nd
在C++ 98中引入了两个绑定适配器bind1st 和bind2nd,它们被用来从一个二元函数对象,产生一个一元函数对象。比如如下例子:
template <typename Container>
void showContainer(Container &c)
{
for (auto i : c)
{
std::cout << i << " ";
}
std::cout << std::endl;
}
int main()
{
vector<int> vec;
srand(time(nullptr));
for (size_t i = 0; i < 20; ++i)
{
vec.push_back(rand() % 100 + 1);
}
showContainer<vector<int>>(vec);
sort(vec.begin(), vec.end());
showContainer(vec);
sort(vec.begin(), vec.end(), greater<int>());
showContainer(vec);
return 1;
}
在代码中,首先定义了一个vector并插入一些随机数,然后调用C++中的排序算法sort,sort默认的第三个参数是一个函数对象,默认是less,即从小到大排序。我们可以自定义一个函数对象,比如这里传入内置的greate函数对象,即可以上实现按从大到小排序的结果,上述代码输出如下:
7 38 37 35 97 29 62 74 87 60 46 53 26 54 71 71 100 34 34 71
7 26 29 34 34 35 37 38 46 53 54 60 62 71 71 71 74 87 97 100
100 97 87 74 71 71 71 62 60 54 53 46 38 37 35 34 34 29 26 7
现在有新需求,假设我们把值70,按照顺序插入到vec容器当中,首先第一步就是找到第一个小于70的数字,在算法库中存在一个find_if函数:
template<typename _InputIterator, typename _Predicate>
_GLIBCXX20_CONSTEXPR
inline _InputIterator
find_if(_InputIterator __first, _InputIterator __last,
_Predicate __pred)
{
// concept requirements
__glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)
__glibcxx_function_requires(_UnaryPredicateConcept<_Predicate,
typename iterator_traits<_InputIterator>::value_type>)
__glibcxx_requires_valid_range(__first, __last);
return std::__find_if(__first, __last,
__gnu_cxx::__ops::__pred_iter(__pred));
}
可以看到,第三个参数,是一个一元函数对象(UnaryPredicateConcept)。所以无法直接使用库中提供的如greater, less这类二元函数对象。这时就需要使用绑定器,来将二元函数对象通过固定1个参数的方式转换为一元函数对象。greater和less的实现如下:
template <typename T>
class greater
{
public:
bool operator()(T lhs, T rhs)
{
return lhs > rhs;
}
};
template <typename T>
class less
{
public:
bool operator()(T lhs, T rhs)
{
return lhs < rhs;
}
};
如果使用bind1st,绑定greater,那就是固定greate的第一个参数,假设这里是70,则bind1st(greater<int>,70),相当于greater只接受第二个rhs参数,内部逻辑变为:
return 70 > rhs;
如果使用bind2nd,绑定less,那就是固定less的第二个参数,假设这里是70,则bind2nd(less<int>,70),相当与less只接受第一个lhs参数,内部逻辑变为:
return lhs < 70;
这两个效果相同,都是找到小于70的那个元素的位置。
auto it = find_if(vec.begin(), vec.end(), bind1st(greater<int>(), 70));
if (it != vec.end())
{
vec.insert(it, 70);
showContainer(vec);
}
输出结果为:
94 94 86 85 79 77 51 50 48 45 45 44 40 36 36 33 21 21 18 15
94 94 86 85 79 77 70 51 50 48 45 45 44 40 36 36 33 21 21 18 15
可以看到,70被插入到了第一个小于70的元素位置。根据以上分析:
auto it = find_if(vec.begin(), vec.end(), bind1st(greater<int>(), 70));
auto it = find_if(vec.begin(), vec.end(), bind2nd(less<int>(), 70));
这两个语句的效果是一样的。
bind1st和bind2nd实现原理
bind1st和bind2nd的实现原理也很简单。上面find_if的实现大概如下,这里为了演示,我们定义一个my_find_if:
template <typename Iterator, typename Compare>
Iterator my_find_if(Iterator first, Iterator last, Compare comp)
{
for (; first != last; ++first)
{
if (comp(*first))
{
return first;
}
}
return last;
}
可以看到,第三个参数是一个一元函数对象,在循环的内部,判断该函数对象的返回值,如果满足条件,直接返回该迭代器。
接下来实现一下my_bind1st,代码如下:
template <typename Compare, typename T>
_mybind1st<Compare, T> my_bind1st(Compare comp, const T &t)
{
return _mybind1st<Compare, T>(comp, t);
}
代码很简单,它有两个参数,第一个参数是一个二元函数对象,第二个参数是一个常量的值。代码内部仅仅是实例化了一个新的名为_mybind1st的对象,它把这两个参数作为构造函数的参数传递进去。
_mybind1st的代码如下:
template <typename Compre, typename T>
class _mybind1st
{
public:
_mybind1st(Compre comp, T val) : _comp(comp), _val(val) {}
bool operator()(const T &second)
{
return _comp(_val, second);
}
private:
Compre _comp;
T _val;
};
可以看到,_mybind1st是一个一元函数对象,它是由二元函数对象comp封装而来,该二元函数的第一个参数已经被构造函数中的val固定,所以operator函数只需要接受第二个参数。在逻辑内部就是调用二元函数对象,固定第一个参数,仅接受第二个函数来实现。
同理,要实现_mybind2nd也很简单:
template <typename Compre, typename T>
class _mybind2nd
{
public:
_mybind2nd(Compre comp, T val) : _comp(comp), _val(val) {}
bool operator()(const T &first)
{
return _comp(first, _val);
}
private:
Compre _comp;
T _val;
};
template <typename Compare, typename T>
_mybind2nd<Compare, T> my_bind2nd(Compare comp, const T &t)
{
return _mybind2nd<Compare, T>(comp, t);
}
bind1st和bind2nd在C++11中已经被弃用,推荐使用bind(本文后续会讲到),因为绑定内置的函数对象看起来很方便,但是如果要绑定用户自定义的函数对象,则需要添加一些特性才可以。
比如,对于下面这两个函数和函数对象:
int sum(int a, int b)
{
return a + b;
}
template <typename T>
class Sum
{
public:
T operator()(T lhs, T rhs) const
{
return lhs + rhs;
}
};
上面这两个分别时二元函数和二元函数对象,它们是不能直接用bind1st或者bind2nd来绑定的,比如假如我们想把Sum函数对象的第一个参数固定为5,如果写出下面的代码:
binder1st<Sum<int>> addFive = bind1st(Sum<int>(), 5);
则会直接报一堆错误:
根据报错的信息,看起来是缺少一些在绑定时需要用到的描述信息。正确的做法是,要给Sum函数对象添加一些描述信息,比如:
template <typename T>
class Sum
{
//https://stackoverflow.com/questions/16625509/c-functor-binding
public:
typedef T result_type;
typedef T first_argument_type;
typedef T second_argument_type;
T operator()(T lhs, T rhs) const
{
return lhs + rhs;
}
};
或者让其继承自内置的binary_function,指明每个函数的参数类型以及返回值类型:
template <typename T>
class Sum : public std::binary_function<T, T, T>
{
public:
T operator()(T lhs, T rhs) const
{
return lhs + rhs;
}
};
现在就能绑定这个函数对象了。
另外如果像绑定自由函数,比如:
auto addFiveFunc = bind1st(&sum, 5);
也是会直接报错的。
解决方法是使用std::ptr_fun对sum函数进行包装。
binder1st<pointer_to_binary_function<int,int,int>> addFiveFunc = bind1st(std::ptr_fun(&sum), 5);
返回值类型可以使用auto来推断。
这两个报错,都提示C++98 中bind1st,bind2nd,ptr_func等这类函数已经被弃用,建议使用C++11中的bind和function来实现相同的功能。
function
用法
C++中提供的绑定器和函数对象,以及lambda表达式,它们只能使用在一条语句中,如果要在多条语句中使用,则要保存他们的类型。function函数对象类型类似于函数指针,但是它的应用比函数指针更广泛,函数指针只能绑定函数,而function函数对象类型则可以绑定除函数之外,还可以绑定函数对象,绑定器以及lambda表达式的函数对象等等。举例如下:
void helloworld()
{
cout << "hello world" << endl;
}
void hello(string str)
{
cout << "hello " << str << endl;
}
int sum(int lhs, int rhs)
{
return lhs + rhs;
}
int main()
{
function<void()> func1 = helloworld;
func1(); // output: hello world
function<void(string)> func2 = hello;
func2("zhangsan"); // output: hello zhangsan
function<int(int, int)> func3 = sum;
cout << func3(10, 20) << endl; // output:30
function<int(int, int)> func4 = [](int a, int b)
{ return a + b; };
cout << func4(20, 30) << endl; // output:50
return 1;
}
function不仅能绑定全局函数,也能绑定成员函数,这一点和成员方法的指针类似。
比如对于上面的hello方法,定义函数指针为:
void (*phello)(string) = hello;
phello("li si");
如果hello是以下类的成员方法,则成员方法函数指针定义如下:
class Test
{
public:
void hello(string str)
{
cout << "hello " << str << endl;
}
};
main()
{
void (Test::*pTestHello)(string) = Test::hello;
Test t;
((&t)->*pTestHello)("wang wu");
(t.*pTestHello)("wang wu2");
}
对于function函数对象类型,第一个参数必须是该成员对象的指针,这相当于成员函数第一个参数默认为this指针。
function<void(Test *, string)> func5 = Test::hello;
Test t;
func5(&t, "zhaoliu");
通过function函数对象调用方法时,相当于调用function函数对象的operator()操作符重载函数。
原理
现在,假设对于hello函数,我们要定义一个自定义的myfunction函数类型对象:
void hello(string str)
{
cout << "hello " << str << endl;
}
main()
{
myfunction<void(string)> func = hello;
func("zhangsan"); // output: hello zhangsan
return 1;
}
myfunction的实现如下:
template <typename Fty>
class myfunction
{
};
template <typename R, typename A1>
class myfunction<R(A1)>
{
public:
using PFUNC = R (*)(A1);
myfunction(PFUNC pfunc) : _pfunc(pfunc) {}
R operator()(A1 arg)
{
return _pfunc(arg);
}
private:
PFUNC _pfunc;
};
可以看到,需要使用函数类型来实例化myfunction,通过myfunction调用operator()函数的时候,需要根据函数类型传入相应的参数。
现在假设要使用myfunction来适配下面的sum函数。
int sum(int lhs, int rhs)
{
return lhs + rhs;
}
用法为:
myfunction<int(int, int)> mfunc2 = sum;
cout << mfunc2(10, 20) << endl;
现在,就需要重新新增一个具有1个返回值,2个参数的函数模板。
template <typename R, typename A1, typename A2>
class myfunction<R(A1, A2)>
{
public:
using PFUNC = R (*)(A1, A2);
myfunction(PFUNC pfunc) : _pfunc(pfunc) {}
R operator()(A1 arg1, A2 arg2)
{
return _pfunc(arg1, arg2);
}
private:
PFUNC _pfunc;
};
现在问题来了,如果要适配一个具有n个参数的方法,那么是不是要写一个具有n个参数的模板方法,这样就太不灵活了。幸好在C++中有可变参数模板类型。所以上面的代码可以简化为带有一个可变模板参数的模板方法:
template <typename R, typename... A>
class myfunction<R(A...)>
{
public:
using PFUNC = R (*)(A...);
myfunction(PFUNC pfunc) : _pfunc(pfunc) {}
R operator()(A... arg)
{
return _pfunc(arg...);
}
private:
PFUNC _pfunc;
};
A类型后面的省略号,表示具有1个或多个模板参数。
bind绑定器
bind是一个绑定器,它可以用来替代之前的bind1st和bind2nd,它返回的结果还是一个函数对象。使用方法如下:
void hello(string str)
{
cout << str << endl;
}
int sum(int a, int b)
{
return a + b;
}
class Test
{
public:
int sum(int a, int b)
{
return a + b;
}
};
int main()
{
bind(hello, "hello bind!")(); //output: hello bind!
cout << bind(sum, 10, 20)() << endl; //output: 30
cout << bind(&Test::sum, Test(), 20, 30)() << endl; // output:50
return 1;
}
另外,bind还可以和参数占位符使用,比如:
bind(hello, placeholders::_1)("hello bind2!");// output: hello bind2!
cout << bind(sum, placeholders::_1, placeholders::_2)(20, 30) << endl; //output: 50
可以使用function来保存bind的返回结果,比如:
function<void(string)> func1 = bind(hello, placeholders::_1);
func1("hello world!"); // output: hello world!
function<int(int)> funcsum = bind(&Test::sum, Test(), 5, placeholders::_1);
cout << "funcsum:" << funcsum(1) << endl; // output: funcsum:6
function<int(int)> funcfreesum = bind(sum, 5, placeholders::_1);
cout << "funcfreesum:" << funcfreesum(1) << endl; // output: funcfreesum:6
bind可以替代之前的bind1st和bind2nd,比如:
binder1st<greater<int>> g1 = bind1st(greater<int>(), 30);
cout << g1(5) << endl; // 30 > 5 output:1
function<bool(int)> g2 = bind(greater<int>(), 30, placeholders::_1);
cout << g2(5) << endl; // 30 > 5 output:1
binder2nd<less<int>> g3 = bind2nd(less<int>(), 30);
cout << g3(5) << endl; // 5< 30 output:1
function<bool(int)> g4 = bind(less<int>(), placeholders::_1, 30);
cout << g4(5) << endl; // 5< 30 output:1
binder1st和function不能直接转换,但可以借助lambda表达式来实现:
比如上面的Sum自定义函数对象,可以像如下这样来转换:
binder1st<Sum<int>> addFive = bind1st(Sum<int>(), 5);
function<int(int)> addFiveFunction = [addFive](int x) -> int
{
return addFive(x);
};
lambda表达式
lambda 表达式是一个源自阿隆佐·邱奇(Alonzo Church)(艾伦·图灵(Alan Turing)的老师)的术语。邱奇创立了 λ 演算,后来被证明和图灵机是等价的。lambda表达式是函数对象的升级。函数对象一般不会单独使用,而是使用在泛型算法的参数传递过程中。比如一些具有比较性质的数据结构(优先级队列的大根堆小根堆定义,智能指针的删除器操作),可以自定义比较操作。函数对象的缺点是:要使用函数对象,必须先定义函数对象类,而一旦用完之后,后面可能就永远用不到了。没必要因为需要用到函数对象,就要定义一个类出来,而这个类型却永远存在在代码当中,灵活性差。
lambda表达式是函数对象的更高级实现,它的基本语法如下:
[捕获外部变量](形参列表)->返回值{操作代码};
- 如果lambda表达式不需要返回值,那么"->返回值"可以省略
- [捕获外部变量] 有如下方式:
- []:不捕获任何外部对象
- [=]:以传值的方式捕获外部的所有变量
- [&]:以传引用的方式捕获外部的所有变量
- [this]:捕获外部的this指针
- [=,&a]:以传值的方式捕获外部所有变量,但是a变量以传引用的方式捕获
- [a,b]:以传值的方式捕获外部变量a和b
- [a, &b]:a以传值方式捕获,b以传引用方式捕获。
原理
lambda表达式就是函数对象,比如下面这个lambda表达式:
auto func1 = []() -> void { cout << "hello world!" << endl; };
func1();
相当于创建了一个函数对象:
template <typename T = void>
class TestLambda01
{
public:
TestLambda01() {}
void operator()()
{
cout << "hello world!" << endl;
}
};
然后使用:
TestLambda01<> t1;
t1();
再比如:
auto func2 = [](int lhs, int rhs) -> int { return lhs + rhs; };
cout << func2(10, 20) << endl;
相当于生产函数对象:
template <typename T>
class TestLambda02
{
public:
T operator()(T a, T b) const { return a + b; }
};
然后使用:
TestLambda02<int> t2;
cout << t2(10, 20) << endl;
需要特别注意的是,上述操作符重载标识为了const,表示是常方法,这意味着在方法内部是不能修改私有变量的。比如下面的方法:
int a = 1;
int b = 2;
auto func3 = [a, b]() -> int
{
int temp = a;
a = b;
b = temp;
};
编译阶段就会有如下报错:
上述lambda表达式的实现的函数对象如下:
template <typename T>
class TestLambda03
{
public:
TestLambda03(T a, T b) : _a(a), _b(b) {}
void operator()() const
{
T temp = _a;
_a = _b;
_b = temp;
}
private:
T _a;
T _b;
};
使用方法如下:
可以看到,小括号运算符重载方法被标记为了const,因此在该方法里不能修改成员变量的值。要修改,需要将成员变量标记为mutable。
template <typename T>
class TestLambda03
{
public:
TestLambda03(T a, T b) : _a(a), _b(b) {}
void operator()() const
{
T temp = _a;
_a = _b;
_b = temp;
}
private:
mutable T _a;
mutable T _b;
};
所以,可以在lambda表达式后面加上mutable,这样就可以对成员变量进行修改。
int a = 1;
int b = 2;
auto func3 = [a, b]() mutable -> int
{
int temp = a;
a = b;
b = temp;
};
参考: