【C++】C++中的lambda表达式和函数对象

目录结构:c++

contents structure [-]

lambda表达式是C++11中引入的一项新技术,利用lambda表达式能够编写内嵌的匿名函数,用以替换独立函数或者函数对象,而且使代码更可读。可是从本质上来说,lambda表达式只是一种语法糖,由于全部其能完成的工做均可以用其它稍微复杂的代码来实现。可是它简便的语法却给C++带来了深远的影响。若是从广义上说,lamdba表达式产生的是函数对象。在类中,能够重载函数调用运算符(),此时类的对象能够将具备相似函数的行为,咱们称这些对象为函数对象(Function Object)或者仿函数(Functor)。相比lambda表达式,函数对象有本身独特的优点。下面咱们开始具体讲解这两项黑科技。算法

1 lambda表达式

咱们先从简答的例子开始,咱们定义一个能够输出字符串的lambda表达式,表达式通常都是从方括号[]开始,而后结束于花括号{},花括号里面就像定义函数那样,包含了lamdba表达式体:编程

// 定义简单的lambda表达式
auto basicLambda = [] { cout << "Hello, world!" << endl; };
// 调用
basicLambda();   // 输出:Hello, world!

上面是最简单的lambda表达式,没有参数。若是须要参数,那么就要像函数那样,放在圆括号里面,若是有返回值,返回类型要放在->后面,即拖尾返回类型,固然你也能够忽略返回类型,lambda会帮你自动推断出返回类型:设计模式

// 指明返回类型
auto add = [](int a, int b) -> int { return a + b; };
// 自动推断返回类型
auto multiply = [](int a, int b) { return a * b; };

int sum = add(2, 5);   // 输出:7
int product = multiply(2, 5);  // 输出:10


你们可能会想lambda表达式最前面的方括号的意义何在?其实这是lambda表达式一个很要的功能,就是闭包。这里咱们先讲一下lambda表达式的大体原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类固然重载了()运算符),咱们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。因此,咱们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其能够经过传值或者引用的方式捕捉其封装做用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,咱们又将其称为lambda捕捉块。看下面的例子:数组

int main()
{
    int x = 10;
    
    auto add_x = [x](int a) { return a + x; };  // 复制捕捉x
    auto multiply_x = [&x](int a) { return a * x; };  // 引用捕捉x
    
    cout << add_x(10) << " " << multiply_x(10) << endl;
    // 输出:20 100
    return 0;
}


当lambda捕捉块为空时,表示没有捕捉任何变量。可是上面的add_x是以复制的形式捕捉变量x,而multiply是以引用的方式捕捉x。前面讲过,lambda表达式是产生一个闭包类,那么捕捉是回事?对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。前面说过,闭包类也实现了函数调用运算符的重载,通常状况是:安全

class ClosureType
{
public:
    // ...
    ReturnType operator(params) const { body };
}


这意味着lambda表达式没法修改经过复制形式捕捉的变量,由于函数调用运算符的重载方法是const属性的。有时候,你想改动传值方式捕获的值,那么就要使用mutable,例子以下:闭包

int main()
{
    int x = 10;
    
    auto add_x = [x](int a) mutable { x *= 2; return a + x; };  // 复制捕捉x
    
    cout << add_x(10) << endl; // 输出 30
    return 0;
}


这是为何呢?由于你一旦将lambda表达式标记为mutable,那么实现的了函数调用运算符是非const属性的:app

class ClosureType
{
public:
    // ...
    ReturnType operator(params) { body };
}


对于引用捕获方式,不管是否标记mutable,均可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,看来与具体实现有关。既然说到了深处,还有一点要注意:lambda表达式是不能被赋值的:less

auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };

a = b;   // 非法,lambda没法赋值
auto c = a;   // 合法,生成一个副本


你可能会想a与b对应的函数类型是一致的(编译器也显示是相同类型:lambda [] void () -> void),为何不能相互赋值呢?由于禁用了赋值操做符:函数

ClosureType& operator=(const ClosureType&) = delete;

可是没有禁用复制构造函数,因此你仍然能够用一个lambda表达式去初始化另一个lambda表达式而产生副本。而且lambda表达式也能够赋值给相对应的函数指针,这也使得你彻底能够把lambda表达式当作对应函数类型的指针。
闲话少说,纳入正题,捕获的方式能够是引用也能够是复制,可是具体说来会有如下几种状况来捕获其所在做用域中的变量:

[]:默认不捕获任何变量;
[=]:默认以值捕获全部变量;
[&]:默认以引用捕获全部变量;
[x]:仅以值捕获x,其它变量不捕获;
[&x]:仅以引用捕获x,其它变量不捕获;
[=, &x]:默认以值捕获全部变量,可是x是例外,经过引用捕获;
[&, x]:默认以引用捕获全部变量,可是x是例外,经过值捕获;
[this]:经过引用捕获当前对象(实际上是复制指针);
[*this]:经过传值方式捕获当前对象;

在上面的捕获方式中,注意最好不要使用[=]和[&]默认捕获全部变量。首先说默认引用捕获全部变量,你有很大可能会出现悬挂引用(Dangling references),由于引用捕获不会延长引用的变量的声明周期:

std::function<int(int)> add_x(int x)
{
    return [&](int a) { return x + a; };
}


由于参数x仅是一个临时变量,函数调用后就被销毁,可是返回的lambda表达式却引用了该变量,但调用这个表达式时,引用的是一个垃圾值,因此会产生没有意义的结果。你可能会想,能够经过传值的方式来解决上面的问题:

std::function<int(int)> add_x(int x)
{
    return [=](int a) { return x + a; };
}


是的,使用默认传值方式能够避免悬挂引用问题。可是采用默认值捕获全部变量仍然有风险,看下面的例子:

class Filter
{
public:
    Filter(int divisorVal):
        divisor{divisorVal}
    {}

    std::function<bool(int)> getFilter()
    {
        return [=](int value) {return value % divisor == 0; };
    }

private:
    int divisor;
};



这个类中有一个成员方法,能够返回一个lambda表达式,这个表达式使用了类的数据成员divisor。并且采用默认值方式捕捉全部变量。你可能认为这个lambda表达式也捕捉了divisor的一份副本,可是实际上大错特错。问题出如今哪里呢?由于数据成员divisor对lambda表达式并不可见,你能够用下面的代码验证:

// 类的方法,下面没法编译,由于divisor并不在lambda捕捉的范围
std::function<bool(int)> getFilter()
{
    return [divisor](int value) {return value % divisor == 0; };
}


那么原来的代码为何可以捕捉到呢?仔细想一想,原来每一个非静态方法都有一个this指针变量,利用this指针,你能够接近任何成员变量,因此lambda表达式实际上捕捉的是this指针的副本,因此原来的代码等价于:

std::function<bool(int)> getFilter()
{
    return [this](int value) {return value % this->divisor == 0; };
}


尽管仍是以值方式捕获,可是捕获的是指针,其实至关于以引用的方式捕获了当前类对象,因此lambda表达式的闭包与一个类对象绑定在一块儿了,这也很危险,由于你仍然有可能在类对象析构后使用这个lambda表达式,那么相似“悬挂引用”的问题也会产生。因此,采用默认值捕捉全部变量仍然是不安全的,主要是因为指针变量的复制,实际上仍是按引用传值。
经过前面的例子,你还能够看到lambda表达式能够做为返回值。咱们知道lambda表达式能够赋值给对应类型的函数指针。可是使用函数指针貌似并非那么方便。因此STL定义在<functional>头文件提供了一个多态的函数对象封装std::function,其相似于函数指针。它能够绑定任何类函数对象,只要参数与返回类型相同。以下面的返回一个bool且接收两个int的函数包装器:

std::function<bool(int, int)> wrapper = [](int x, int y) { return x < y; };


而lambda表达式一个更重要的应用是其能够用于函数的参数,经过这种方式能够实现回调函数。其实,最经常使用的是在STL算法中,好比你要统计一个数组中知足特定条件的元素数量,经过lambda表达式给出条件,传递给count_if函数:

int value = 3;
vector<int> v {1, 3, 5, 2, 6, 10};

int count = std::count_if(v.beigin(), v.end(), [value](int x) { return x > value; });


再好比你想生成斐波那契数列,而后保存在数组中,此时你可使用generate函数,并辅助lambda表达式:

vector<int> v(10);
int a = 0;
int b = 1;
std::generate(v.begin(), v.end(), [&a, &b] { int value = b; b = b + a; a = value; return value; });
// 此时v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}


此外,lambda表达式还用于对象的排序准则:

class Person
{
public:
    Person(const string& first, const string& last):
        firstName{first}, lastName{last}
    {}

    Person() = default;

    string first() const { return firstName; }
    string last() const { return lastName; }
private:
    string firstName;
    string lastName;
};

int main()
{
    vector<Person> vp;
    // ... 添加Person信息

    // 按照姓名排序
    std::sort(vp.begin(), vp.end(), [](const Person& p1, const Person& p2)
    { return p1.last() < p2.last() || (p1.last() == p2.last() && p1.first() < p2.first()); });
        // ...
    return 0;
}


总之,对于大部分STL算法,能够很是灵活地搭配lambda表达式来实现想要的效果。
前面讲完了lambda表达式的基本使用,最后给出lambda表达式的完整语法:

// 完整语法
[ capture-list ] ( params ) mutable(optional) constexpr(optional)(c++17) exception attribute -> ret { body }

// 可选的简化语法
[ capture-list ] ( params ) -> ret { body }     
[ capture-list ] ( params ) { body }    
[ capture-list ] { body }


第一个是完整的语法,后面3个是可选的语法。这意味着lambda表达式至关灵活,可是照样有必定的限制,好比你使用了拖尾返回类型,那么就不能省略参数列表,尽管其多是空的。针对完整的语法,咱们对各个部分作一个说明:


capture-list:捕捉列表,这个不用多说,前面已经讲过,记住它不能省略;
params:参数列表,能够省略(可是后面必须紧跟函数体);
mutable:可选,将lambda表达式标记为mutable后,函数体就能够修改传值方式捕获的变量;
constexpr:可选,C++17,能够指定lambda表达式是一个常量函数;
exception:可选,指定lambda表达式能够抛出的异常;
attribute:可选,指定lambda表达式的特性;
ret:可选,返回值类型;
body:函数执行体。

若是想了解更多,能够参考 cppreference lambda。

2 lambda c++14新特性

在C++14中,lambda又获得了加强,一个是泛型lambda表达式,一个是lambda能够捕捉表达式。这里咱们对这两项新特色进行简单介绍。

2.1 lambda捕捉表达式

前面讲过,lambda表达式能够按复制或者引用捕获在其做用域范围内的变量。而有时候,咱们但愿捕捉不在其做用域范围内的变量,并且最重要的是咱们但愿捕捉右值。因此C++14中引入了表达式捕捉,其容许用任何类型的表达式初始化捕捉的变量。看下面的例子:

// 利用表达式捕获,能够更灵活地处理做用域内的变量
int x = 4;
auto y = [&r = x, x = x + 1] { r += 2; return x * x; }();
// 此时 x 更新为6,y 为25

// 直接用字面值初始化变量
auto z = [str = "string"]{ return str; }();
// 此时z是const char* 类型,存储字符串 string


能够看到捕捉表达式扩大了lambda表达式的捕捉能力,有时候你能够用std::move初始化变量。这对不能复制只能移动的对象很重要,好比std::unique_ptr,由于其不支持复制操做,你没法以值方式捕捉到它。可是利用lambda捕捉表达式,能够经过移动来捕捉它:

auto myPi = std::make_unique<double>(3.1415);

auto circle_area = [pi = std::move(myPi)](double r) { return *pi * r * r; };
cout << circle_area(1.0) << endl; // 3.1415


其实用表达式初始化捕捉变量,与使用auto声明一个变量的机理是相似的。

2.2 泛型lambda表达式

从C++14开始,lambda表达式支持泛型:其参数可使用自动推断类型的功能,而不须要显示地声明具体类型。这就如同函数模板同样,参数要使用类型自动推断功能,只须要将其类型指定为auto,类型推断规则与函数模板同样。这里给出一个简单例子:

auto add = [](auto x, auto y) { return x + y; };

int x = add(2, 3);   // 5
double y = add(2.5, 3.5);  // 6.0

 

3 函数对象

函数对象是一个普遍的概念,由于全部具备函数行为的对象均可以称为函数对象。这是一个高级抽象,咱们不关心对象究竟是什么,只要其具备函数行为。所谓的函数行为是指的是可使用()调用并传递参数:

function(arg1, arg2, ...);   // 函数调用


这样来讲,lambda表达式也是一个函数对象。可是这里咱们所讲的是一种特殊的函数对象,这种函数对象其实是一个类的实例,只不过这个类实现了函数调用符():

class X
{
public:
    // 定义函数调用符
    ReturnType operator()(params) const;
    
    // ...
};


这样,咱们可使用这个类的对象,并把它当作函数来使用:

X f;
// ...
f(arg1, arg2); // 等价于 f.operator()(arg1, arg2);


仍是例子说话,下面咱们定义一个打印一个整数的函数对象:

// T须要支持输出流运算符
template <typename T>
class Print
{
public:
    void operator()(T elem) const
    {
        cout << elem << ' ' ;
    }
};


int main()
{
    vector<int> v(10);
    int init = 0;
    std::generate(v.begin(), v.end(), [&init] { return init++; });

    // 使用for_each输出各个元素(送入一个Print实例)
    std::for_each(v.begin(), v.end(), Print<int>{});
    // 利用lambda表达式:std::for_each(v.begin(), v.end(), [](int x){ cout << x << ' ';});
    // 输出:0, 1, 2, 3, 4, 5, 6, 7, 8, 9
    return 0;
}


能够看到Print<int>的实例能够传入std::for_each,其表现能够像函数同样,所以咱们称这个实例为函数对象。你们可能会想,for_each为何能够既接收lambda表达式,也能够接收函数对象,其实STL算法是泛型实现的,其不关心接收的对象究竟是什么类型,可是必需要支持函数调用运算:

// for_each的相似实现
namespace std
{
    template <typename Iterator, typename Operation>
    Operation for_each(Iterator act, Iterator end, Operation op)
    {
        while (act != end)
        {
            op(*act);
            ++act;
        }
        return op;
    }
}


泛型提供了高级抽象,不管是lambda表达式、函数对象,仍是函数指针,均可以传入for_each算法中。
本质上,函数对象是类对象,这也使得函数对象相比普通函数有本身的独特优点:


函数对象带有状态:函数对象相对于普通函数是“智能函数”,这就如同智能指针相较于传统指针。由于函数对象除了提供函数调用符方法,还能够拥有其余方法和数据成员。因此函数对象有状态。即便同一个类实例化的不一样的函数对象其状态也不相同,这是普通函数所没法作到的。并且函数对象是能够在运行时建立。

每一个函数对象有本身的类型:对于普通函数来讲,只要签名一致,其类型就是相同的。可是这并不适用于函数对象,由于函数对象的类型是其类的类型。这样,函数对象有本身的类型,这意味着函数对象能够用于模板参数,这对泛型编程有很大提高。

函数对象通常快于普通函数:由于函数对象通常用于模板参数,模板通常会在编译时会作一些优化。

这里咱们看一个能够拥有状态的函数对象,其用于生成连续序列:

class IntSequence
{
public:
    IntSequence(int initVal) : value{ initVal } {}

    int operator()() { return ++value; }
private:
    int value;
};


int main()
{
    vector<int> v(10);
    std::generate(v.begin(), v.end(), IntSequence{ 0 });
    /*  lambda实现一样效果
        int init = 0;
        std::generate(v.begin(), v.end(), [&init] { return ++init; });
    */
    std::for_each(v.begin(), v.end(), [](int x) { cout << x << ' '; });
    //输出:1, 2, 3, 4, 5, 6, 7, 8, 9, 10
    
    return 0;
}


能够看到,函数对象能够拥有一个私有数据成员,每次调用时递增,从而产生连续序列。一样地,你能够用lambda表达式实现相似的效果,可是必须采用引用捕捉方式。可是,函数对象能够实现更复杂的功能,而用lambda表达式则须要复杂的引用捕捉。考虑一个能够计算均值的函数对象:

class MeanValue
{
public:
    MeanValue(): num{0}, sum{0} {}

    void operator()(int e)
    {
        ++num;
        sum += num;
    }

    double value()
    { return static_cast<double>(sum) / static_cast<double>(num); }
private:
    int num;
    int sum;
};


int main()
{
    vector<int> v{ 1, 3, 5, 7 };
    MeanValue mv = std::for_each(v.begin(), v.end(), MeanValue{});
    cout << mv.value() << endl;  // output: 2.5

    return 0;
}


能够看到MeanValue对象中保存了两个私有变量num和sum分别记录数量与总和,最后能够经过二者计算出均值。lambda表达式也能够利用引用捕捉实现相似功能,可是会有点繁琐。这也算是函数对象独特的优点。
头文件<functional>中预约义了一些函数对象,如算术函数对象,比较函数对象,逻辑运算函数对象及按位函数对象,咱们能够在须要时使用它们。好比less<>是STL排序算法中的默认比较函数对象,因此默认的排序结果是升序,可是若是你想降序排列,你可使用greater<>函数对象:

vector<int> v{3, 4, 2, 9, 5};
// 升序排序
std::sort(v.begin(), v.end());  // output: 2, 3, 4, 5, 9
// 降序排列
std::sort(v.begin(), v.end(), std::greater<int>{}); // output: 9, 5, 4, 3, 2

更多有关函数对象的信息你们能够参考这里。

4 函数适配器

从设计模式来讲,函数适配器是一种特殊的函数对象,是将函数对象与其它函数对象,或者特定的值,或者特定的函数相互组合的产物。因为组合特性,函数适配器能够知足特定的需求,头文件<functional>定义了几种函数适配器:

std::bind(op, args...):将函数对象op的参数绑定到特定的值args
std::mem_fn(op):将类的成员函数转化为一个函数对象
std::not1(op), std::not2(op):一元取反器和二元取反器

 

4.1 绑定器(binder)

绑定器std::bind是最经常使用的函数适配器,它能够将函数对象的参数绑定至特定的值。对于没有绑定的参数可使用std::placeholers::_1, std::placeholers::_2等标记。咱们从简单的例子开始,好比你想获得一个减去固定树的函数对象:

auto minus10 = std::bind(std::minus<int>{}, std::placeholders::_1, 10);
cout << minus10(20) << endl;  // 输出10


有时候你能够利用绑定器从新排列参数的顺序,下面的绑定器交换两个参数的位置:

// 逆转参数顺序
auto vminus = std::bind(std::minus<int>{}, std::placeholders::_2, std::placeholders::_1);
cout << vminus(20, 10) << endl;  // 输出-10


绑定器还能够互相嵌套,从而实现函数对象的组合:

// 定义一个接收一个参数,而后将参数加10再乘以2的函数对象
auto plus10times2 = std::bind(std::multiplies<int>{},
        std::bind(std::plus<int>{}, std::placeholders::_1, 10), 2);
cout << plus10times2(4) << endl; // 输出: 28

// 定义3次方函数对象
auto pow3 = std::bind(std::multiplies<int>{},
        std::bind(std::multiplies<int>{}, std::placeholders::_1, std::placeholders::_1),
        std::placeholders::_1);
cout << pow3(3) << endl;  // 输出:27


利用不一样函数对象组合,函数适配器能够调用全局函数,下面的例子是不区分大小写来判断一个字符串是否包含一个特定的子串:

// 大写转换函数
char myToupper(char c)
{
    if (c >= 'a' && c <= 'z')
        return static_cast<char>(c - 'a' + 'A');
    return c;
}

int main()
{
    string s{ "Internationalization" };
    string sub{ "Nation" };

    auto pos = std::search(s.begin(), s.end(), sub.begin(), sub.end(),
                        std::bind(std::equal_to<char>{},
                            std::bind(myToupper, std::placeholders::_1),
                            std::bind(myToupper, std::placeholders::_2)));
    if (pos != s.end())
    {
        cout << sub << " is part of " << s << endl;
    }
    // 输出:Nation is part of Internationalization
    return 0;
}


注意绑定器默认是以传值方绑定参数,若是须要引用绑定值,那么要使用std::ref和std::cref函数,分别表明普通引用和const引用绑定参数:

void f(int& n1, int& n2, const int& n3)
{
    cout << "In function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    ++n1;
    ++n2;
    // ++n3;  //没法编译
}

int main()
{
    int n1 = 1, n2 = 2, n3 = 3;
    auto boundf = std::bind(f, n1, std::ref(n2), std::cref(n3));
    n1 = 10;
    n2 = 11;
    n3 = 12;
    cout << "Before function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    boundf();
    cout << "After function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    //  Before function : 10 11 12
    //  In function : 1 11 12
    //  After function : 10 12 12

    return 0;
}


能够看到,n1是以默认方式绑定到函数f上,故仅是一个副本,不会影响原来的n1变量,可是n2是以引用绑定的,绑定到f的参数与原来的n2相互影响,n3是以const引用绑定的,函数f没法修改其值。
绑定器能够用于调用类中的成员函数:

class Person
{
public:
    Person(const string& n) : name{ n } {}
    void print() const { cout << name << endl; }
    void print2(const string& prefix) { cout << prefix << name << endl; }
private:
    string name;
};
int main()
{
    vector<Person> p{ Person{"Tick"}, Person{"Trick"} };
    // 调用成员函数print
    std::for_each(p.begin(), p.end(), std::bind(&Person::print, std::placeholders::_1));
    // 此处的std::placeholders::_1表示要调用的Person对象,因此至关于调用arg1.print()
    // 输出:Tick   Trick
    std::for_each(p.begin(), p.end(), std::bind(&Person::print2, std::placeholders::_1,
        "Person: "));
    // 此处的std::placeholders::_1表示要调用的Person对象,因此至关于调用arg1.print2("Person: ")
    // 输出:Person: Tick   Person: Trick

    return 0;
}


并且绑定器对虚函数也有效,你能够本身作一下测试。
前面说过,C++11中lambda表达式没法实现移动捕捉变量,可是使用绑定器能够实现相似的功能:

vector<int> data{ 1, 2, 3, 4 };
auto func = std::bind([](const vector<int>& data) { cout << data.size() << endl; },
                           std::move(data));
func();  // 4
cout << data.size() << endl;  // 0


能够看到绑定器能够实现移动语义,这是由于对于左值参数,绑定对象是复制构造的,可是对右值参数,绑定对象是移动构造的。

4.2 std::mem_fn()适配器

当想调用成员函数时,你还可使用std::mem_fn函数,此时你能够省略掉用于调用对象的占位符:

vector<Person> p{ Person{ "Tick" }, Person{ "Trick" } };
std::for_each(p.begin(), p.end(), std::mem_fn(&Person::print));
// 输出: Trick Trick
Person n{ "Bob" };
std::mem_fn(&Person::print2)(n, "Person: ");
// 输出:Person: Bob


因此,使用std::men_fn不须要绑定参数,能够更方便地调用成员函数。再看一个例子,std;:mem_fn还能够调用成员变量:

class Foo
{
public:
    int data = 7;
    void display_greeting() { cout << "Hello, world.\n"; }
    void display_number(int i) { cout << "number: " << i << '\n'; }
    
};
int main()
{
    Foo f;
    // 调用成员函数
    std::mem_fn(&Foo::display_greeting)(f);  // Hello, world.
    std::mem_fn(&Foo::display_number)(f, 20);  // number: 20
    // 调用数据成员
    cout << std::mem_fn(&Foo::data)(f) << endl;  // 7

    return 0;
}


取反器std::not1与std::not2很简单,就是取函数对象的反结果,不过在C++17二者被弃用了,因此就不讲了。

 

原文连接:https://www.jianshu.com/p/d686ad9de817

相关文章
相关标签/搜索