Bootstrap

C++11 lambda函数

用过脚本语言的童鞋都知道,函数的定义可以在任何地方,例如:

local function table_sort(t)
    local function sort_by_len(t1, t2)
        return #t1 < #t2
    end
    
    table.sort(t, sort_by_len)
    
    
    --上面的代码也可以简写为:
    --table.sort(t, function(t1, t2)
    --    return #t1 < # t2
    --end)
end

我们应该提倡这种内部函数的用法,他能够让代码更加简洁,而且耦合度更低,因为他在外部是不可见的,我们不需要或不允许被外部可见。遗憾的是在C++11之前没有所谓的这种内部函数,从C++11开始支持内部函数,他有个学名lambda函数,或者lambda表达式。以下是C++11 一个lambda函数的一个例子:

void main()
{
    auto val_lam = [](int x) {return x * x + 1; };
    printf("a is %d\n", val_lam(1));
}

从上面的代码看出,lambda函数比较实用,也比较简单。一般我们使用lambda函数实现一些简短独立的功能,因此lambda函数一般是孤立存在的,也就是说不会访问外部变量。但是并没有规定他不允许访问外部变量,所以lambda函数需要处理变量与外部变量的关系。说的直白一点就是,如果在lambda函数中改变了一个外部变量,那么这个外部变量真的会改变吗?例如将上面的代码变为:

void main()
{
    int x = 10;
    auto val_lam = []() {x++; return x + 1; };
    printf("a is %d\n", val_lam());    //lambda函数的值是多少,现在x是多少?
}

请问val_lam()的返回值是多少,i又是多少呢?答案是程序根本就编译不过。为什么会这样子?为了安全起见,lambda函数引用外部变量时,必须先在[]中捕获外部变量,也就是这样:

void main()
{
    int x = 3;
    int y = 5;
    auto val_lam = [x,y]() {x++; return x + y; };
    printf("a is %d\n", val_lam());    //lambda函数的值是多少,现在x是多少?
}

编译还是报错!提示说x,y是只读变量。还是安全原因,C++11默认把lambda函数设定为const函数。要想改变外部函数的值,必须去掉const属性,对C++比较了解的应该知道,加上关键字mutable就可以了,即:

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

我们把上面引用外部变量的方式叫做值捕获,即不会改变外部变量的值,只是传递值的作用。其实lambda函数中的[x,y]可以简写为[=],编译器会自动推导出相关的变量x,y。

值捕获的坑

关于值捕获要注意的地方是,与参数传值类似,值捕获的前提是变量可以拷贝,不同之处则在于,被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝,例如:

void main()
{
    int x = 10;
    auto val_lam = [=]() {return x + 1; };
    x = 20;
    printf("a is %d\n", val_lam());    //lambda函数的值为11,而不是21
}

我们看到lambda函数的值为11,而不是21。因为值捕获在创建时就传入了。

值捕获还有一个需要注意的地方,不容易理解,也是其特性,就是外部值的连续性:

void main()
{
	int x = 10;
    auto val_lam = [=]() mutable {x++; return x + 1; };

    printf("a is %d\n", val_lam());     //12 
	printf("a is %d\n", val_lam());     //13
	printf("a is %d\n", val_lam());     //14
	printf("x is %d\n", x);             //此时x还是10
}

看到没,多次调用lambda函数,发现其记住了外部变量的值。几年前学习到这个特性的时候,也是很不能理解,后来用多了lua的闭包函数和upvalue,我现在可以会心的一笑了。事实上利用这个特性可以实现高阶函数,这个我在lua里用的比较多。

引用捕获

与值捕获对应的就是引用捕获,引用捕获可以真正改变外部的值,因为引用捕获就是引用的外部变量地址。

void main()
{
	int x = 10;
    auto val_lam = [&]() mutable {x++; return x + 1; };

    printf("a is %d\n", val_lam());     //12 
	printf("a is %d\n", val_lam());     //13
	printf("a is %d\n", val_lam());     //14
	printf("x is %d\n", x);             //此时x已经变为13
}

我们看到引用捕获其实和函数参数传引用类似,作用也是一致的,一方面可以改变变量的值,另一方面在面向对象编程时,引用捕获相比值捕获可以省去新对象的构造,节省开销。

完整定义

我们一步步介绍了lambda函数的用法,实际上我们还没有对lambda函数的完整定义做介绍。事实上这也是我自己写文章和学习的风格,如果还没有接触一个概念之前,就完整介绍概念的定义,用法,那就是典型的教科书式写法。这是我很反对的,因为学习是一个循序渐进的过程,最好的办法就是用最少最简单的,已知的知识方式介绍一个概念,否则一上来就介绍大段的定义和理论,是会吓坏新手的。

好了,我们看看lambda函数的完整定义:

1、捕获子句(lambda表达式从此处开始,也叫做lambda-introducer。)

2、参数列表(可选),也叫做lambda声明符(lambda declarator)。

3、mutable 选项(可选),加上mutable后,lambda表达式体内的语句可以修改按值捕获的变量。

4、异常选项(可选),加上throw()表示lambda表达式不抛出任何异常(别搞反了)。

5、尾部返回类型(trailing-return-type) (可选),一般来讲lambda表达式的返回值都可以由编译器自动猜测除非你指明了尾部返回类型。

6、lambda 本体,也就是函数体部分。

以上有些定义是我们不常见的,等遇到时再去查看具体用法,这样有针对性的学习才是有效率的。

 

欢迎加入QQ群 858791125 讨论skynet,游戏后台开发,lua脚本语言等问题。

 

;