函数对象

概述

函数对象是一个可以被当作函数来用的对象。

根据惯例,使用了 struct 关键字而不是 class 关键字:

struct funObjAdd
{
public:
    funObjAdd(){}
    funObjAdd(int _n) : n(_n){}
    // 定义 operator()运算符允许像调用函数一样使用小括号的语法。
    int operator()(int i) const
    {
        return n + i;
    }
private:
    int n{ 0 };
};
funObjAdd f1(1);    	//C++98
auto f2 = funObjAdd(1); //C++11

函数指针和引用

除非用一个引用模板参数来捕捉函数类型,传递给一个函数的函数实参会退化成为一个函数指针。不管是函数指针还是函数引用,都可以当成函数对象来用。

int fun(int x)
{
    return x * x;
}

template <typename F>
auto test1(F f)
{
    return f(3);
}

template <typename F>
auto test2(F& f)
{
    return f(3);
}

template <typename F>
auto test3(F* f)
{
    // return f(3) is OK
    return (*f)(3);
}
cout << test1(fun) << endl;	//int (*)(int)
cout << test2(fun) << endl;	//int (&)(int)
cout << test3(fun) << endl; //int (*)(int)

注意在函数指针的情况下,直接写 *value 也可以。

很多接收函数对象的地方,也可以接收函数的指针或引用。但在个别情况下,需要通过函数对象的类型来区分函数对象的时候,就不能使用函数指针或引用了——原型相同的函数,它们的类型也是相同的。

Lambda 表达式

  • Lambda 表达式以一对中括号开始

  • 跟函数定义一样,有参数列表

  • 跟正常的函数定义一样,会有一个函数体,里面会有 return 语句

  • 一般不需要说明返回值(相当于 auto);有特殊情况要说明,使用箭头语法:[](int x) -> int { … }

  • 每个 lambda 表达式都有一个全局唯一类型,要精确捕捉 lambda 表达式到一个变量中,只能 auto 声明的方式

    lambda 本质不是函数指针,可以认为编译器组合出了一个唯一的类型名称。

    每个 lambda 都有自己的独特类型,每次定义相当于编译器帮你产生了一个函数对象(就像定义的那些函数对象一样)。

    下面是一个lambda表达式反汇编的部分代码:(1f9b1ea5c4d835764b4a74b83856e332像MD5算法生成的)

    00A51C78  call        <lambda_1f9b1ea5c4d835764b4a74b83856e332>::operator() (0A518E0h) 
// 加法
auto my_add = [](int a) {return [a](int b) {return a + b; }; };
cout << my_add(1)(2) << endl;
// vector遍历
vector<int> v{ 2,3,4};
transform(v.begin(), v.end(), v.begin(), [](int a) {return a * a; }); //4,9,16

lambda 表达式可以立即进行求值,它免去了我们定义一个 constexpr 函数的必要。

只要能满足 constexpr 函数的条件,一个 lambda 表达式默认就是 constexpr 函数。

[](int x) { return x * x + x; }(3);

反汇编可以查看到寄存器eax的值为12。

    [](int x) { return x * x + x; }(3);
00A51C68  xor         eax,eax  ;12
00A51C6A  mov         byte ptr [ebp-0C5h],al  
00A51C70  push        3  
00A51C72  lea         ecx,[ebp-0C5h]  
00A51C78  call        <lambda_1f9b1ea5c4d835764b4a74b83856e332>::operator() (0A518E0h) 

lambda 解决多重初始化路径。

Obj obj;
switch (init_mode) {
case init_mode1:
  obj = Obj(...);
  break;
case init_mode2;
  obj = Obj(...);
  break;
...
}

上面实际上是调用了默认构造函数、带参数的构造函数和(移动)赋值函数:既可能有性能损失,也对 Obj 提出了有默认构造函数的额外要求。对于这样的代码,有一种重构意见是把这样的代码分离成独立的函数。不过,有时候更直截了当的做法是用一个 lambda 表达式来进行改造,既可以提升性能(不需要默认函数或拷贝 / 移动),又让初始化部分显得更清晰:

auto obj = [init_mode]() {
  switch (init_mode) {
  case init_mode1:
    return Obj(...);
    break;
  case init_mode2:
    return Obj(...);
    break;
  ...
  }
}();

变量捕获细节

变量捕获开头是可选的默认捕获符 = 或 &,会自动按值或按引用捕获用到的本地变量,后面跟(逗号分隔):

  • 本地变量名标明对其按值捕获(不能在默认捕获符 = 后,因其已自动按值捕获所有本地变量)
  • & 加本地变量名标明对其按引用捕获(不能在默认捕获符 & 后,因其已自动按引用捕获所有本地变量)
  • this 标明按引用捕获外围对象(对 lambda 表达式定义出现在一个非静态类成员内的情况);注意默认捕获符 = 和 & 号可以自动捕获 this(并且在 C++20 之前,在 = 后写 this 会导致出错)
  • *this 标明按值捕获外围对象(对 lambda 表达式定义出现在一个非静态类成员内的情况;C++17)
  • 变量名 = 表达式 标明按值捕获表达式的结果(可理解为 auto 变量名 = 表达式)
  • &变量名 = 表达式 标明按引用捕获表达式的结果(可理解为 auto& 变量名 = 表达式)

一般而言,按值捕获是比较安全的做法。按引用捕获时则需要更小心些,必须能够确保被捕获的变量和 lambda 表达式的生命期至少一样长,并在有下面需求之一时才使用:

  • 需要在 lambda 表达式中修改这个变量并让外部观察到
  • 需要看到这个变量在外部被修改的结果
  • 这个变量的复制代价比较高

如果希望以移动的方式来捕获某个变量的话,则应考虑 变量名 = 表达式 的形式。表达式可以返回一个 prvalue 或 xvalue,比如可以是 std::move(需移动捕获的变量)。

泛型 lambda 表达式

函数的返回值可以 auto,但参数还是要声明的。在 lambda 表达式里则更进一步,在参数声明时就可以使用 auto(包括 auto&& 等形式)。

template <typename T, typename V>
auto add(T t, V v)
{
    return t + v;
}

auto add = [](auto t, auto v)
{
    return t + v;
};

bind

bind1st 和 bind2nd 目前已经从 C++ 标准里移除,原因:

  • 功能可以被 lambda 表达式替代
  • 更强大的 bind 模板
vector<int> v{ 2,3,4};
transform(v.begin(), v.end(), v.begin(), bind(plus<int>(), placeholders::_1, 1));//3,4,5

bind功能可以被 lambda 表达式替代,对 bind 只需要稍微了解一下就好,在 C++14 之后的年代里,已经没有什么地方必须要使用 bind 了。

function

每一个 lambda 表达式都是一个单独的类型,所以只能使用 auto 或模板参数来接收结果。在很多情况下,我们需要使用一个更方便的通用类型来接收,这时就可以使用 function 模板。function 模板的参数就是函数的类型,一个函数对象放到 function 里之后,外界可以观察到的就只剩下它的参数、返回值类型和执行效果了。

map<string, function<int(int, int)>>
op_dict{
    {"+", [](int x, int y) { return x + y; }},
    {"-", [](int x, int y) { return x - y; }},
    {"*", [](int x, int y) { return x * y; }},
    {"/", [](int x, int y) { return x / y; }},
};

注意 function 对象的创建还是比较耗资源的,所以只在用 auto 等方法解决不了问题的时候使用这个模板。