C++ function、bind以及lamda表达式

——C++学习笔记

Posted by Samuel on August 1, 2017

目录

闭包(closure)

闭包是一个函数与它所引用的非本地变量的上下文环境的集合。闭包可以访问它定义范围之外的变量,最常见的应用是回调函数。

function

在C++中,可以调用的实体主要包括函数、函数指针、函数引用、可以隐式转换为函数指定的对象,或者实现了operator()的对象(即C++98中的仿函数(functor))

在C++11中,新增加了std::function对象,它是对C++中实现的现有可调用实体的一种类型安全的包裹。

下面是一个function对象的例子:

#include <functional>
 
std::function<size_t (const char*)> print_func;
 
// normal function -> std::function object
size_t CPrint(const char*) { ... }
print_func = CPrint;
print_func("hello world"):
 
// functor -> std::function object
class CxxPrint {
public:
    size_t operator()(const char*) { ... }
};
CxxPrint p;
print_func = p;
print_func("hello world");

在上面的例子中,我们把一个普通的函数和一个functor赋值给了一个std::function对象,然后我们通过该对象来调用。其它的C++中的可调用实体都可以像上面一样来使用。通过std::function的包裹,我们可以像传递普通的对象一样来传递可调用实体,这样就很好解决了类型安全的问题。了解了std::function的基本用法,下面我们来看一些使用过程中的注意事项:

  1. 关于可调用实体转换为std::function对象需要遵守以下两条原则:
    1. 转换后的std::function对象的参数能转换为可调用实体的参数
    2. 可高用实体的返回值能转换为std::function对象的(这里注意,所有的可调用实体的返回值都与返回void的std::function对象的返回值兼容)。
  2. std::function对象可以refer to满足(1)中条件的任意可调用实体
  3. std::function object最大的用处就是在实现函数回调,使用者需要注意,它不能被用来检查相等或者不相等

bind

bind是这样一种机制,它可以预先把指定可调用实体的某些参数绑定到已有的变量,产生一个新的可调用实体,这种机制在回调函数的使用过程中也颇为有用。下面我们通过例子,来看看bind的用法:

#include <functional>
 
int Func(int x, int y);
auto bf1 = std::bind(Func, 10, std::placeholders::_1);
bf1(20); ///< same as Func(10, 20)
 
class A {
public:
    int Func(int x, int y);
};
 
A a;
auto bf2 = std::bind(&A::Func, a, std::placeholders::_1, std::placeholders::_2);
bf2(10, 20); ///< same as a.Func(10, 20)
 
std::function< int(int)> bf3 = std::bind(&A::Func, a, std::placeholders::_1, 100);
bf3(10); ///< same as a.Func(10, 100)

上面的例子中,bf1是把一个两个参数普通函数的第一个参数绑定为10,生成了一个新的一个参数的可调用实体体; bf2是把一个类成员函数绑定了类对象,生成了一个像普通函数一样的新的可调用实体; bf3是把类成员函数绑定了类对象和第二个参数,生成了一个新的std::function对象。看懂了上面的例子,下面我们来说说使用bind需要注意的一些事项:

  1. bind预先绑定的参数需要传具体的变量或值进去,对于预先绑定的参数,是pass-by-value
  2. 对于不事先绑定的参数,需要传std::placeholders进去,从_1开始,依次递增。placeholder是pass-by-reference
  3. bind的返回值是可调用实体,可以直接赋给std::function对象
  4. 对于绑定的指针、引用类型的参数,使用者需要保证在可调用实体调用之前,这些参数是可用的
  5. 类的this可以通过对象或者指针来绑定

lambda

lambda就是用来实现closure的,它的最大用途也是在回调函数。

vector<int> vec;
/// 1. simple lambda
auto it = std::find_if(vec.begin(), vec.end(), [](int i) { return i > 50; });
class A {
public:
    bool operator(int i) const { return i > 50; }
};
auto it = std::find_if(vec.begin(), vec.end(), A());
 
/// 2. lambda return syntax
std::function<int(int)> square = [](int i) -> int { return i * i; }
 
/// 3. lambda expr: capture of local variable
{
    int min_val = 10;
    int max_val = 1000;
 
    auto it = std::find_if(vec.begin(), vec.end(), [=](int i) { // ‘=’ pass-by-value
        return i > min_val && i < max_val; 
        });
 
    auto it = std::find_if(vec.begin(), vec.end(), [&](int i) { // ‘&’ pass-by-reference
        return i > min_val && i < max_val;
        });
 
    auto it = std::find_if(vec.begin(), vec.end(), [=, &max_value](int i) {
        return i > min_val && i < max_val;
        });
}
 
/// 4. lambda expr: capture of class member
class A
{
public:
    void DoSomething();
 
private:
    std::vector<int>  m_vec;
    int               m_min_val;
    int               m_max_va;
};
 
/// 4.1 capture member by this
void A::DoSomething()
{
    auto it = std::find_if(m_vec.begin(), m_vec.end(), [this](int i){
        return i > m_min_val && i < m_max_val; });
}
 
/// 4.2 capture member by default pass-by-value
void A::DoSomething()
{
    auto it = std::find_if(m_vec.begin(), m_vec.end(), [=](int i){
        return i > m_min_val && i < m_max_val; });
}
 
/// 4.3 capture member by default pass-by-reference
void A::DoSomething()
{
    auto it = std::find_if(m_vec.begin(), m_vec.end(), [&](int i){
        return i > m_min_val && i < m_max_val; });
}

上面的例子基本覆盖到了lambda表达的基本用法。我们一个个来分析每个例子(标号与上面代码注释中1,2,3,4一致):

  1. 这是最简单的lambda表达式,可以认为用了lambda表达式的find_if和下面使用了functor的find_if是等价的
  2. 这个是有返回值的lambda表达式,返回值的语法如上面所示,通过->写在参数列表的括号后面。返回值在下面的情况下是可以省略的:
    1. 返回值是void的时候
    2. lambda表达式的body中有return expr,且expr的类型与返回值的一样
  3. 这个是lambda表达式capture本地局部变量的例子,这里三个小例子,分别是capture时不同的语法,第一个小例子中=表示capture的变量pass-by-value, 第二个小拿出中&表示capture的变量pass-by-reference,第三个小例子是说指定了default的pass-by-value, 但是max_value这个单独pass-by-reference
  4. 这个是lambda表达式capture类成员变量的例子,这里也有三个小例子。第一个小例子是通过this指针来capture成员变量,第二、三个是通过缺省的方式,只不过第二个是通过pass-by-value的方式,第三个是通过pass-by-reference的

分析完了上面的例子,我们来总结一下关于lambda表达式使用时的一些注意事项:

  1. lambda表达式要使用引用变量,需要遵守下面的原则:
    1. 在调用上下文中的局部变量,只有capture了才可以引用(如上面的例子3所示)
    2. 非本地局部变量可以直接引用
  2. 使用者需要注意,closure(lambda表达式生成的可调用实体)引用的变量(主要是指针和引用),在closure调用完成之前,必须保证可用,这一点和上面bind绑定参数之后生成的可调用实体是一致的
  3. 关于lambda的用处,就是用来生成closure,而closure也是一种可调用实体,所以可以通过std::function对象来保存生成的closure,也可以直接用auto