C++11/C++14 中的 Lambda 与 Objective-C 中的 Block

当年上大学我们专业分 C++ (包括 .NET ) 和 Java 两个方向,而我原本是 C++ 方向的,后来阴差阳错走上了 Java -> J2EE -> Android 这条“不归路”,以至于不少当年的同学都以为我一开始是 Java 方向的。

不过当年因为课程设置的原因,《面向对象程序设计( C++ )》和《面向对象程序设计( Java )》是相对应的,都过分强调面向对象那一块,除此之外印象比较深的就只有运算符重载这一块了。

12 年学 Cocos2d-X 游戏开发,总算又有机会重拾 C++,但依然没有去系统的温习 C++ 语言本身。

现在主要写 Android / iOS / Golang,虽然很少直接接触 C++,但涉及到 [音视频和图形图像处理] 、[后端网络服务和数据存储基础设施] 这两大块领域,还是离不开 C++。

最近一个月“工作量不饱和”,忙里偷闲通过《 C++ Primer 第 5 版》重新温习了一遍 C++。写的 Demo 在这里

着重看了以前没怎么接触过的 template 这块内容,以及众多 C++11 新特性:auto 自动类型推导、shared_ptr / unique_ptr / weak_ptr 智能指针、Lambda 、Enum Class 强类型枚举等。

而印象最深的就是 Lambda 这块内容,可能是因为前不久刚刚学习过 Haskell 这门纯函数式编程语言。

C++11 开始引入 Lambda ,前不久刚发布的 C++14 又加入了一些新特性。

而 Objective-C 中通过 Block 也提供了类似的支持。

C++11 中的 Lambda

使用 Lambda 需要 #include <functional> ,用 GCC 或 Clang 编译时需要加入参数 -std=c++11

定义 Lambda

Lambda 的类型为 std::function , 其一般表达形式为:

[capture](parameters) -> return_type { function_body }

如果没有参数, parameters 这部分可以省略。

return_type 这部分也可以省略: 如果 function_body 中包含了 return 语句,则可以推导出返回类型;如果没有,则默认返回类型为 void 。个人认为最好还是不要省略,这样能增强代码可读性。

例如定义一个传入两个 double 类型参数、返回值也为 double 类型的 Lambda , 其类型应为 std::function<double(double, double)>

std::function<double(double, double)> multiply;
multiply = [] (double x, double y) -> double {
	return x * y;
};

Objective-C 中 Block 的定义基本是与此类似的:

double (^multiply)(double x, double y);
multiply = ^(double x, double y)
{
	return x * y;
};

而考虑到 C++11 中引入了 auto 自动类型推导,上面的 C++ 版本的 Lambda 定义还可以更简洁:

auto multiply = [] (double x, double y) -> double {
	return x * y;
};

捕获外部变量

C++ 中 Lambda 访问外部变量的几种情况:

  • [] : 不使用任何外部变量;

  • [=] : 通过拷贝的方式使用外部所有变量;

  • [var] : 通过拷贝的方式使用外部变量 var ;

  • [&] : 通过引用的方式使用外部所有变量;

  • [&var] : 通过引用的方式使用外部变量 var ;

  • [=, &var] : 除了 var 通过引用的方式使用,其他所有外部变量均通过拷贝的方式使用;

  • [&, var] : 除了 var 通过拷贝的方式使用,其他所有外部变量均通过引用的方式使用;

如果通过引用的方式使用的外部变量 var 不是 const 的,则可以在 Lambda 中修改它的值。

下面定义一个求圆面积的 Lambda ,它需要访问并修改外部变量 pi :

double pi = 3.14;
auto circleSpace = [=, &pi] (double radius) -> double {
	pi = 3.1415926535;
	return pi * radius * radius;
};
std::cout << "Result: " << circleSpace(1.23456789) << std::endl;

此外,如果要在 Lambda 中改变外部变量的值,还可以通过在参数列表后添加 mutable 关键字来实现:

auto circleSpace = [=] (double radius) mutable -> double {
	pi = 3.1415926535;
	return pi * radius * radius;
};

而在 Objective-C 的 Block 中,如果需要修改外部变量,需要在该变量前面加上关键字 __block

__block double pi = 3.14;
double (^circleSpace)(double radius);
circleSpace = ^(double radius)
{
	pi = 3.1415926535;
	return pi * radius * radius;
}
NSLog(@"Result: %f", circleSpace(1.23456789));

Lambda 的其他用法

C++ 中的 for_each 第三个参数可以为一个 callable 对象,因此使用 Lambda 可以更方便地遍历 vector 等容器对象:

std::vector<char> v;
for (int i = 0; i < 26; i++) {
	v.push_back((char)('A' + i));
}
for_each(v.begin(), v.end(), [] (char c) -> void {
	std::cout << c << std::endl;
});

C++14 中的 Lambda 新特性

如果使用了 C++14 中的 Lambda 新特性,GCC / Clang 编译参数需要加上 -std=c++1y

Generic Lambda

Lambda 的参数类型也可以声明为 auto 了!相当于泛型。

依然以上面的 multiply 为例:

auto multiply = [] (auto x, auto y) {
	return x * y;
};
std::cout << multiply(-1234, 5.6789) << std::endl;
std::cout << multiply('A', 'Z') << std::endl;

Initialized Lambda Captures

Capture 语句支持初始化表达式了!

假设有一个 unique_ptr 类型的指针,如果要在 Lambda 内部访问它,就必须先用 move 转移所有权,或者用 release() 释放所有权。现在,你可以直接将这一步放在 Capture 语句中完成:

std::unique_ptr<std::string> nameP(new std::string("Rinc"));
auto readName = [nameP = move(nameP)] {
	return *nameP;
};
/*
auto readName = [nameP = nameP.release()] {
	return *nameP;
};
*/
std::cout << readName() << std::endl;

当然,用 , 分隔的多个初始化表达式作为 Capture 也是可以的:

std::shared_ptr<std::string> str1, str2;
auto print = [
	str1 = std::make_shared<std::string>("Rinc"),
	str2 = std::make_shared<std::string>("Liu")
	] () { 
	std::cout << *str1 << " " << *str2 << std::endl;
};
print();