在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;

这两种方式的区别在于:

  1. 使用函数是无法内联的,在编译阶段,编译器无法确定该调用哪个函数,因为传递给模板方法的是一个函数指针。而使用函数对象,则在编译阶段,就能确定函数的调用方法,这样能去除函数调用的开销,从而提升应用程序效率。这应该就是C++中引入函数对象的主要原因。
  2. 函数对象是由类生成的,所以还可以添加相关的成员变量,用来记录函数对象使用时的更多信息

在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表达式是函数对象的更高级实现,它的基本语法如下:

[捕获外部变量](形参列表)->返回值{操作代码};

  1. 如果lambda表达式不需要返回值,那么"->返回值"可以省略
  2. [捕获外部变量] 有如下方式:
    • []:不捕获任何外部对象
    • [=]:以传值的方式捕获外部的所有变量
    • [&]:以传引用的方式捕获外部的所有变量
    • [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;
};

 

 

参考: