在 c++ 中,一个可以执行的任务有多种形式:函数,lambda 表达式,std::function 对象,std::bind 绑定的任务,std::packaged_task 封装的任务。
1 lambda 表达式
lambda 表达式类似于匿名函数,在需要传递一段计算逻辑时,往往不想也不需要把这段逻辑声明定义到一个函数里,这个时候就可以使用 lambda 表达式。使用 lambda 表达式,就像使用一个局部变量一样方便,在函数内部声明,也可以将 lambda 表达式当成一个参数进行传递。
lambda 表达式的格式如下:
[捕获列表](形参列表) -> 选项 返回值类型 {函数体}
如下是一个 lambda 表达式的例子,在捕获列表中捕获了 base,函数形参是两个整型数,函数体中计算了两个整型数的以及 base 的和,返回值是 int 类型。
lambda 表达式函数体内只可以使用捕获列表中的变量,形参列表中的变量。形参列表是使用 lambda 表达式时传递过来的参数,捕获列表是捕获的外部的变量。
#include <iostream>
#include <string>
int main() {
int base = 100;
auto sum_func = [base](int a, int b) -> int {
return base + a + b;
};
std::cout << sum_func(1, 2) << std::endl;
return 0;
}
1.1 值捕获,引用捕获
值捕获与引用捕获和调用函数时的值传递和引用传递的语义是类似的。值捕获,在创建表达式的时候,已经把值捕获了,也就是说创建表达式之后再修改这个值,对 lambda 表达式也是没有影响的;引用捕获,在表达式调用的时候才会去取值,所以如果创建表达式之后又修改了引用的值,那么表达式中捕获的值也会发生变化。
如下代码,两个 lambda 表达式,分别对 base做了值捕获和引用捕获。在捕获之前,base 值是 100,在创建 lambda 表达式之后,将 base 的值修改为 200,之后再执行 lambda 表达式。可以看到值捕获的 base 没有发生改变,还是 100;引用捕获的 base 变成了 200。
#include <iostream>
#include <string>
int main() {
int base = 100;
auto sum_func_value = [base](int a, int b) -> int {
return base + a + b;
};
auto sum_func_ref = [&base](int a, int b) -> int {
return base + a + b;
};
base = 200;
std::cout << "lambda value captutre: " << sum_func_value(1, 2) << std::endl;
std::cout << "lambda ref captutre: " << sum_func_ref(1, 2) << std::endl;
return 0;
}
1.2 隐式捕获
如果捕获的参数比较多的话,又不想一个一个写,这个时候可以使用隐式捕获。
(1)隐式捕获也分隐式值捕获和隐式引用捕获:[=],[&]
(2)隐式捕获可以和显式捕获混合使用,但是要注意两点规范
① 隐式捕获需要放在捕获列表的第一个位置
② 隐式引用捕获和显式值捕获可以混合使用,隐式值捕获和显式引用捕获可以混合使用;隐式引用捕获和显式引用捕获可以在一块使用,但是回报编译告警;隐式值捕获和显式值捕获也可以在一块使用,也会有编译告警。
#include <iostream>
#include <string>
int main() {
int base = 100;
int extend = 200;
auto sum_func1 = [=]() -> int {
return base + extend;
};
auto sum_func2 = [&]() -> int {
return base + extend;
};
auto sum_func3 = [&, base]() -> int {
return base + extend;
};
// 隐式引用捕获和显式引用捕获
// 一般不这么用,因为都是引用捕获,只使用隐式捕获就可以了
// 这样使用会有编译告警
// 隐式引用捕获和显式值捕获
// 隐式值捕获和显式引用捕获,一般这么用
auto sum_func4 = [&, &base]() -> int {
return base + extend;
};
auto sum_func5 = [=, &base]() -> int {
return base + extend;
};
// 隐式捕获需要放在捕获列表的第一个位置
/*
auto sum_func6 = [&base, =]() -> int {
return base + extend;
};
*/
// 隐式捕获可以和显式捕获混合使用
// 但是,隐式捕获需要放在捕获列表的第一个位置
/*
auto sum_func7 = [base, &]() -> int {
return base + extend;
};
*/
base = 110;
extend = 210;
std::cout << "sum func1: " << sum_func1() << std::endl;
std::cout << "sum func2: " << sum_func2() << std::endl;
std::cout << "sum func3: " << sum_func3() << std::endl;
std::cout << "sum func4: " << sum_func4() << std::endl;
std::cout << "sum func5: " << sum_func5() << std::endl;
return 0;
}
1.3 表达式捕获
如下是一个表达式捕获的例子,在捕获列表中是一个表达式,表达式左边的值不需要指定类型,指定类型会报编译错误。
#include <iostream>
#include <string>
int main() {
int base = 100;
int extend = 200;
// 表达式捕获不需要指定参数类型
auto sum_func_value = [/*int*/ value = base + extend](int a, int b) -> int {
return value + a + b;
};
std::cout << "lambda value captutre: " << sum_func_value(1, 2) << std::endl;
return 0;
}
1.4 选项
默认情况下,捕获的值是只读的,不能修改;如果选项中使用 mutable 关键字修饰,则可以修改。
#include <iostream>
#include <string>
int main() {
int base = 100;
// 默认情况下,捕获的值是只读的,不能修改
// 所以在函数内修改会报编译错误
/*
auto func1 = [base]() -> int {
base = base + 1;
return base;
};
*/
// 选项中使用 mutable,那么捕获的变量在函数体内可以修改
auto func2 = [base]() mutable -> int {
base = base + 2;
return base;
};
// std::cout << "func1: " << func1() << std::endl;
std::cout << "func2: " << func2() << std::endl;
return 0;
}
2 std::function
std::function 可以是一个函数,一个 lambda 表达式或者是一个通过 std::bind 封装的任务。std::function 为函数作为参数传递提供了方便。
如下是 std::function 中的一个示例代码。
#include <functional>
#include <iostream>
struct Foo
{
Foo(int num) : num_(num) {
std::cout << "Foo(), num_ = " << num_ << std::endl;
}
void print_add(int i) const { std::cout << num_ + i << '\n'; }
int num_;
};
void print_num(int i)
{
std::cout << i << '\n';
}
struct PrintNum
{
void operator()(int i) const
{
std::cout << i << '\n';
}
};
int main()
{
// 普通函数
std::cout << "普通函数 ----------------\n";
std::function<void(int)> f_display = print_num;
f_display(-9);
// lamda 表达式
std::cout << "lambda 表达式 ----------------\n";
std::function<void()> f_display_42 = []() { print_num(42); };
f_display_42();
// std::bind 包装的任务
std::cout << "std::bind 普通函数 ----------------";
std::function<void()> f_display_31337 = std::bind(print_num, 31337);
f_display_31337();
// 保存一个成员函数
// 使用成员函数的时候需要指定一个对象
std::cout << "类成员函数 ----------------";
std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
const Foo foo(314159);
f_add_display(foo, 1);
f_add_display(314160, 1);
// store a call to a data member accessor
// 还能使用一个类成员进行赋值 ??? 这种使用方法很少见
std::cout << "成员变量 ----------------";
std::function<int(Foo const&)> f_num = &Foo::num_;
std::cout << "num_: " << f_num(foo) << '\n';
// store a call to a member function and object
// std::bind 类的成员函数
std::cout << "std::bind 成员函数 ----------------";
using std::placeholders::_1;
std::function<void(int)> f_add_display2 = std::bind(&Foo::print_add, foo, _1);
f_add_display2(2);
// store a call to a member function and object ptr
std::function<void(int)> f_add_display3 = std::bind(&Foo::print_add, &foo, _1);
f_add_display3(3);
// store a call to a function object
std::cout << "类重载符 ----------------";
std::function<void(int)> f_display_obj = PrintNum();
f_display_obj(18);
std::cout << "递归调用 ----------------";
auto factorial = [](int n)
{
// store a lambda object to emulate "recursive lambda"; aware of extra overhead
std::function<int(int)> fac = [&](int n) { return (n < 2) ? 1 : n * fac(n - 1); };
// note that "auto fac = [&](int n) {...};" does not work in recursive calls
return fac(n);
};
for (int i{5}; i != 8; ++i)
std::cout << i << "! = " << factorial(i) << "; ";
std::cout << '\n';
}
在使用 std::function 的时候,也经常使用下边的方式。
(1)使用 using 定义函数类型
(2)std::function 的模板类型就是使用 using 定义的函数类型
当写一个框架,接收外部传递进来的函数时,常常这么使用。using 定义的函数类型,还可以当做一个类型,对函数地址进行类型转换。
#include <iostream>
#include <string>
#include <functional>
using callback = int(int a, int b);
std::function<callback> func;
int sum(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int main() {
func = sum;
std::cout << func(1, 2) << std::endl;
func = (callback *)sub;
std::cout << func(1, 2) << std::endl;
auto sum_addr = (void *)sum;
std::cout << "sum addr = " << sum_addr << std::endl;
func = (callback *)sum_addr;
std::cout << func(10, 20) << std::endl;
return 0;
}
3 std::packaged_task
std::package_task,从名字也可以看出来,表示一个打包的任务。这个任务自己并不能执行,而是需要显式的调用来执行。
如下是官网的示例代码。
(1)task 能够封装的任务类型包括 lambda 表达式,bind 的函数,也可以是一个单纯的函数。
std::packaged_task 是对执行任务进行的封装,封装之后 std::packaged_task 还有自己的属性,比如 std::future 可以获取任务执行的结果;std::function 可以用一个任务进行赋值,没有增加其它属性。
(2)与 std::promise 类似,std::packaged_task 也可以获取一个 std::future,std::future 可以获取到 std::package_task 执行的结果
std::packaged_task - cppreference.com
#include <cmath>
#include <functional>
#include <future>
#include <iostream>
#include <thread>
// unique function to avoid disambiguating the std::pow overload set
int f(int x, int y) { return std::pow(x, y); }
void task_lambda()
{
std::packaged_task<int(int, int)> task([](int a, int b)
{
return std::pow(a, b);
});
std::future<int> result = task.get_future();
// 封装一个 lamda 表达式,可以将 task 当成函数名来直接调用
task(2, 9);
std::cout << "task_lambda:\t" << result.get() << '\n';
}
void task_bind()
{
std::packaged_task<int()> task(std::bind(f, 2, 11));
std::future<int> result = task.get_future();
// 封装的 bind() 函数,可以直接调用
task();
std::cout << "task_bind:\t" << result.get() << '\n';
}
void task_thread()
{
std::packaged_task<int(int, int)> task(f);
std::future<int> result = task.get_future();
// 非 bind 方式,不能这样直接调用
// std::cout << "directly call: " << task(2, 10) << std::endl;
std::thread task_td(std::move(task), 2, 10);
task_td.join();
std::cout << "task_thread:\t" << result.get() << '\n';
}
int main()
{
task_lambda();
task_bind();
task_thread();
return 0;
}