leetcode 的 submission 是收到 IO 速度的影响的……
尽管和其他 OJ 网站不同, leetcode 给你提供了完备的代码级的接口,它的测试例仍旧是通过 IO 来读取的,真是让人桑心。
明白这一点是因为,我在查看某道题目的最快解时,发现了这么一段代码。平心而论,他的代码并不比我的复杂度要简化多少,然而却比我快10倍以上,我本来百思不得其解,直到我发现了这么一段代码:
static int fast_io = [](){
std::ios::sync_with_stdio(false);
cin.tie(nullptr);
return 0;
}();
这段代码利用 lambda 表达式完成了在全局作用于定义并且立即执行,而其所做的无非也就是两件事:
- 用
sync_with_stdio
接口,关闭std::cin
和std::cout
与scanf
和printf
的同步,减少了相当的 IO 开销。 - 用
cin.tie
接口,完成了cin
和cout
的解耦,减少了大量 flush 调用。
由此大大提升了 IO 效率,给 submission 一个更漂亮的速度。
那么lambda 表达式(C++11起)是什么呢?
基本语法如下:
1.[ captures ] <tparams>(可選)(C++20) ( params ) specifiers(可選) exception attr -> ret requires(可選)(C++20) { body }
2.[ captures ] ( params ) -> ret { body }
3.[ captures ] ( params ) { body }
4.[ captures ] { body }
1) 完整聲明。
2) const lambda 的聲明:以複製捕獲的對象在 lambda 體內為 const 。
3) 省略尾隨返回類型:從 return 語句推導閉包的 operator()
的返回類型,如同對於聲明返回類型為 auto 的函數。
4) 省略參數列表:函數不接收參數,如同參數列表是 ()
。僅若不使用 constexpr
、 mutable
、異常規定、屬性或尾隨返回類型之一才能使用此形式。
C++中,一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。它与普通函数不同的是,lambda必须使用尾置返回来指定返回类型。
例如调用<algorithm>中的std::sort,ISO C++ 98 的写法是要先写一个compare函数:
bool compare(int& a,int& b)
{
return a>b;
}
然后,再这样调用:
sort(a, a+n, compare);
然而,用ISO C++ 11 标准新增的Lambda表达式,可以这么写:
sort(a, a+n, [](int a,int b){return a>b;});//降序排序
这样一来,代码明显简洁多了。
由于Lambda的类型是单一的,不能通过类型名来显式声明对应的对象,但可以利用auto关键字和类型推导:
auto f=[](int a,int b){return a>b;};
和其它语言的一个较明显的区别是Lambda和C++的类型系统结合使用,如:
auto f=[x](int a,int b){return a>x;}; //x被捕获复制
int x=0, y=1;
auto g=[&](int x){return ++y;}; //y被捕获引用,调用g后会修改y,需要注意y的生存期
bool(*fp)(int, int)=[](int a,int b){return a>b;};//不捕获时才可转换为函数指针
Lambda表达式可以嵌套使用。
出版的ISO C++14支持基于类型推断的泛型lambda表达式。上面的排序代码可以这样写:
sort( a, a+n, [](const auto& a,const auto& b){return a>b;} );//降序排序:不依赖a和b的具体类型
因为参数类型和函数模板参数一样可以被推导而无需和具体参数类型耦合,有利于重构代码;和使用auto声明变量的作用类似,它也允许避免书写过于复杂的参数类型。特别地,不需要显式指出参数类型使使用高阶函数变得更加容易。
捕获选项
[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 { // 函数体 }
所谓捕获列表,其实可以理解为参数的一种类型,lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的,这时候捕获列表可以起到传递外部数据的作用。
- [] Capture nothing (or, a scorched earth strategy?)
- [&] Capture any referenced variable by reference
- [=] Capture any referenced variable by making a copy
- [=, &foo] Capture any referenced variable by making a copy, but capture variable foo by reference
- [bar] Capture bar by making a copy; don’t copy anything else
- [this] Capture the this pointer of the enclosing class
根据传递的行为,捕获列表也分为以下几种:
1. 值捕获
#include <iostream>
using namespace std;
void learn_lambda_func_3() {
auto add = [v1 = 1.2, v2 = 2](int x, int y) -> double{
return x + y + v1 + v2;
};
std::cout << "add(3, 4) = " << add(3, 4) << std::endl;
}
int main()
{
learn_lambda_func_3();
return 0;
}
输出:
add(3, 4) = 10.2
与参数传值类似,值捕获的前期是变量可以拷贝。不同之处则在于,被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝:
#include <iostream>
using namespace std;
void learn_lambda_func_1() {
int value_1 = 1;
auto copy_value_1 = [value_1] {
return value_1;
};
value_1 = 100;
auto stored_value_1 = copy_value_1();
// 这时, stored_value_1 == 1, 而 value_1 == 100.
// 因为 copy_value_1 在创建时就保存了一份 value_1 的拷贝
cout << "value_1 = " << value_1 << endl;
cout << "stored_value_1 = " << stored_value_1 << endl;
}
int main()
{
learn_lambda_func_1();
return 0;
}
输出结果:
value_1 = 100
stored_value_1 = 1
2. 引用捕获
与引用传参类似,引用捕获保存的是引用,值会发生变化。
void learn_lambda_func_2() {
int value_2 = 1;
auto copy_value_2 = [&value_2] {
return value_2;
};
value_2 = 100;
auto stored_value_2 = copy_value_2();
// 这时, stored_value_2 == 100, value_1 == 100.
// 因为 copy_value_2 保存的是引用
cout << "value_2 = " << value_2 << endl;
cout << "stored_value_2 = " << stored_value_2 << endl;
}
输出结果:
value_2 = 100
stored_value_2 = 100
3. 隐式捕获
手动书写捕获列表有时候是非常复杂的,需要我们在捕获列表中显示列出Lambda表达式中使用的外部变量。但是,开发者想了一个办法,就是将这种机械性的工作可以交给编译器来处理。让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获。
隐式捕获有两种方式,分别是[=]
和[&]
。[=]
表示以值捕获的方式捕获外部变量,[&]
表示以引用捕获的方式捕获外部变量。
#include <iostream>
using namespace std;
class test
{
public:
void hello() {
cout << "test hello!\n";
};
void lambda() {
auto fun = [this] { // 捕获了 this 指针
this->hello(); // 这里 this 调用的就是 class test 的对象了
};
fun();
}
};
int main()
{
//显示传递
cout << "/*******显示传递*******/" << endl;
int i = 12; cout << &i << endl; //输出:010FF834 //012(8进制)十进制是10。
auto func = [i] { cout << &i << " " << i <<endl; };
func(); // 输出:010FF828 12
cout << endl;
//隐式传递
cout << "/*******隐式传递*******/" << endl;
[](int j) { cout << &j << " " << j << endl; }(23); //或通过“函数体”后面的‘()’传入参数
cout << endl;
cout << "1.拷贝捕获" << endl;
int a = 123; cout << &a << endl; //输出:010FF81C
auto f = [=] { cout << &a << " "<<a << endl; };
f(); //输出:010FF810 123
cout << endl;
cout << "2.引用捕获" << endl;
int b = 234;
auto f_1 = [&] { cout << &b << " " << b<<endl; };
b = 345; cout << &b << endl; //输出:010FF804
f_1(); //输出:010FF804 345
cout << endl;
cout << "3.拷贝与引用混合" << endl;
//[&, x],变量x以引用形式捕获,其余变量以传值形式捕获
int c = 456, d = 567;
cout << &c << " " << &d << endl; //输出:010FF7EC 010FF7E0
auto f_2 = [=, &d] {
cout << &c << " " << &d <<" "<< c << " " << d << endl; //输出:010FF7D4 010FF7E0 456 567
};
f_2();
cout << endl;
cout << "4.[bar] 指定引用或拷贝" << endl;
int e = 678; cout << &e << endl; //输出:010FF7C4
auto f_3 = [e] { cout << &e << " " << e << endl; };
f_3(); // 输出:010FF7B8 678
cout << endl;
cout << "5.[this] 捕获 this 指针" << endl;
//我们要跳到类中了
test t;
t.lambda();
return 0;
}
Lambda表达式还支持混合的方式捕获外部变量,这种方式主要是以上几种捕获方式的组合使用。
捕获形式 | 说明 |
---|---|
[] | 不捕获任何外部变量 |
[变量名, …] | 默认以值得形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获,需要显示声明(使用&说明符) |
[this] | 以值的形式捕获this指针 |
[=] | 以值的形式捕获所有外部变量 |
[&] | 以引用形式捕获所有外部变量 |
[=, &x] | 变量x以引用形式捕获,其余变量以传值形式捕获 |
[&, x] | 变量x以值的形式捕获,其余变量以引用形式捕获 |
4. 表达式捕获(C++14)
上面提到的值捕获、引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左值,而不能捕获右值。
C++14 给与了我们方便,允许捕获的成员用任意的表达式进行初始化,这就允许了右值的捕获,被声明的捕获变量类型会根据表达式进行判断,判断方式与使用 auto
本质上是相同的:
在下面的代码中,important
是一个独占指针,是不能够被捕获到的,这时候我们需要将其转移为右值,在表达式中初始化。
#include <iostream>
#include <utility>
void learn_lambda_func_3() {
auto important = std::make_unique<int>(1);
auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
return x + y + v1 + (*v2);
};
std::cout << "add(3, 4) = " << add(3, 4) << std::endl;
}
int main()
{
learn_lambda_func_3();
return 0;
}
输出结果:
add(3, 4) = 9
修改捕获变量
前面我们提到过,在Lambda表达式中,如果以传值方式捕获外部变量,则函数体中不能修改该外部变量,否则会引发编译错误。那么有没有办法可以修改值捕获的外部变量呢?这是就需要使用mutable关键字,该关键字用以说明表达式体内的代码可以修改值捕获的变量,示例:
int main()
{
int a = 123; cout << &a << endl;
auto f = [a]()mutable {cout << ++a <<endl; };//不会报错
cout << a << endl;//输出123
f();//输出124
cout << &a << endl;
}
输出结果:
006FF954
123
124
006FF954
嵌套 Lambda 表达式
你可以将 lambda 表达式嵌套在另一个中,如下例所示。 内部 lambda 表达式将其自变量与 2 相乘并返回结果。 外部 lambda 表达式通过其自变量调用内部 lambda 表达式并在结果上加 3。
// nesting_lambda_expressions.cpp
// compile with: /EHsc /W4
#include <iostream>
int main()
{
using namespace std;
// The following lambda expression contains a nested lambda
// expression.
int timestwoplusthree = [](int x) { return [](int y) { return y * 2; }(x) + 3; }(5);
// Print the result.
cout << timestwoplusthree << endl;
}
输出:
13
泛型 Lambda (C++14)
我们曾提到了 auto
关键字不能够用在参数表里,这是因为这样的写法会与模板的功能产生冲突。但是 Lambda 表达式并不是普通函数,所以 Lambda 表达式并不能够模板化。这就为我们造成了一定程度上的麻烦:参数表不能够泛化,必须明确参数表类型。
幸运的是,这种麻烦只存在于 C++11 中,从 C++14 开始,Lambda 函数的形式参数可以使用 auto
关键字来产生意义上的泛型:
void learn_lambda_func_4() {
auto generic = [](auto x, auto y) {
return x + y;
};
std::cout << "generic(1,2) = " << generic(1, 2) << std::endl;
std::cout << "generic(1.1,2.2) = " << generic(1.1, 2.2) << std::endl;
}
输出:
generic(1,2) = 3
generic(1.1,2.2) = 3.3
参考文章:实验楼、Lellansin's 冰森、docs.microsoft - Lambda 表达式的示例