c++可以重载绝大多数运算符,其中包含函数调用操作符()
。当一个类重载了函数调用操作符后,此类型的对象可以像普通函数一样调用,这种对象称为函数对象。如:
1
2
3
4
5
6
|
template<typename T>
struct greater {
bool operator()(const T &lhs, const T &rhs) const {
return lhs > rhs;
}
};
|
使用方式为:
1
2
3
4
5
6
|
greater<int> g;
if (g(1, 2))
// ...
std::vector<int> A;
std::sort(A.begin(), A.end(), greater<int>{});
|
函数对象的类型就是普通的自定义类型,可以有成员变量、构造函数和析构函数等,语法没有区别。和普通函数相比,函数对象有两个优点:1、编译器可以内联执行函数对象的调用;2、函数对象内部可以保持状态,这使得函数对象可以捕获外部变量的值并保存下来,这是c++实现闭包的关键。
在之前已经接触过三个函数对象:default_random_engine
、uniform_real_distribution<T>
和random_device
。它们都定义于头文件<random>
,和标准命名空间std
。其中default_random_engine
为实现定义的随机数生成器,c++标准规定必须实现的算法有:
random_device
是使用硬件熵源的非确定随机数生成器。一般用于上表中随机数生成器的初始化。
uniform_real_distribution
是随机数分布算法,用于将随机数生成器的结果按一定的统计概率密度函数分布。c++标准规定的随机数分布有:
default_random_engine
和random_device
的函数调用操作符的签名为:
1
|
result_type operator()();
|
而uniform_real_distribution
的签名为:
1
2
|
template< class Generator >
result_type operator()(Generator &g);
|
因此生成均匀分布的随机数的方法为:
1
2
3
4
5
6
|
std::random_device rd;
std::default_random_engine re{rd()}
std::uniform_real_distribution<double> dist{0, 10};
for (int i = 0; i < 10; i++)
A[i] = dist(re); // 由dist.operator()来调用re()生成随机数,再映射至区间[0, 10]
|
c++中除普通函数外,可以调用的对象还有函数对象、成员函数和lambda表达式,它们具有不同的类型和属性。为统一c++中所有具有函数行为的对象类型,c++11引入了函数包装器,使函数成为可以像变量一样,具有创建、赋值、返回等操作的一等公民(first class)。函数包装器定义于头文件functional
,定义为:
1
2
3
|
// functional
template <class R, class... Args>
class function<R(Args...)>;
|
使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
#include <iostream>
#include <functional>
// 普通函数
int getOne() { return 1; }
// 函数对象
struct getTwo {
int operator()() { return 2; }
};
// 成员函数
struct getNumber {
// 非static成员函数
int getThree() { return 3; }
// static成员函数
static int getFour() { return 4; }
};
int main() {
std::function<int()> getNumber1{getOne};
std::function<int()> getNumber2{getTwo{}};
std::function<int(class getNumber *)> getNumber3{&getNumber::getThree};
std::function<int()> getNumber4{&getNumber::getFour};
std::function<int()> getNumber5 = []{ return 5; };
getNumber get_number;
std::cout << getNumber1() << ", "
<< getNumber2() << ", "
<< getNumber3(&get_number) << ", " // 非static成员函数行为略有不同
<< getNumber4() << ", "
<< getNumber5() << "\n";
return 0;
}
|
执行效果:1, 2, 3, 4, 5
。
std::function
具有变量的行为,如上面的getNumber1
、getNumber2
和getNumber4
可相互赋值,也可以传参和返回:
1
2
3
4
|
std::function<int()> f(std::function<int()> g) { return g; }
getNumber1 = getNumber2;
getNumber2 = f(getNumber4);
|
有一个值得注意的地方是getNumber2
的初始化歧义。在传统的c++中,对象的初始化语法使用圆括号包围参数,如:
1
2
|
getTwo get_two(); // 默认初始化
std::function<int()> getNumber2(getTwo()); // 先构造临时对象,再将它作为参数传递
|
很不幸,这两条语句都被c++编译器当作函数声明。这是比较容易看出来出错的,还有一些比较复杂的情况,如:
1
2
|
std::ifstream file("data.txt");
std::string str(std::istream_iterator<char>(file), std::istream_iterator<char>());
|
c++标准规定:在直接初始化的变量声明和函数声明有歧义的情况下,c++编译器始终选择函数声明。
为解决这个问题,c++11标准引入直接初始化列表,即以大括号包围初始化参数,以下三个都为变量声明:
1
2
3
|
getTwo get_two{};
std::function<int()> getNumber2{getTwo{}};
std::string str{std::istream_iterator<char>{file}, std::istream_iterator<char>{}};
|
但要注意这个方法也不是十全十美的,如:
1
2
|
std::vector<int> vec(10, 1);
std::vector<int> vec{10, 1};
|
如果{}
内的变量类型全部一致则可能被当成std::initializer_list<T>
,如果存在以std::initializer_list<T>
为参数的构造函数则优先采用。
lambda表达式即闭包,或称为匿名函数。在c++中,lamdba表达式是能够捕获作用域中的变量的无名函数对象。语法为:
1
|
[捕获列表](形参列表) 说明符 -> 返回类型 { 函数体 }
|
其中形参列表、说明符和返回类型都是可选的。
简单直观起见,此处使用两个实例来说明lambda表达式和函数对象的对应关系:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
int a;
std::vector<int> vec;
[a, &vec](int c, const std::vector<int> &d) mutable -> void {
a = c;
vec = d;
};
class ClosureType {
int a; // 复制捕获
std::vector<int> &vec; // 引用捕获
public:
ClosureType(int a, std::vector<int> &vec) : a{a}, vec{vec} {}
void operator()(int c, std::vector<int> &d) {
a = c;
vec = d;
}
};
[a](int b) -> bool { return b < a; };
class ClosureType {
int a;
public:
ClosureType(int a) : a{a} {}
bool operator()(int b) const {
return b < a;
}
};
|
可见,捕获列表中的变量成为函数对象的成员变量,根据复制捕获或者引用捕获的方式来决定成员变量是否为引用类型。参数列表和返回类型分别成为operator()
的参数列表和返回类型。说明符可选mutable
,如果lambda表达式省略mutable
,则operator()
声明为const成员函数,不可更改捕获列表中的变量。
lambda的捕获列表除了可以将变量一一列出之外,还可以使用=
和&
捕获当前上下文的所有变量:
1
2
3
4
|
[&]() {}; // 以引用方式捕获所有变量
[=]() {}; // 以复制方式捕获所有变量
[&, i]() {}; // i以复制方式捕获,其它变量以引用方式捕获
[=, &i]() {}; // i以引用方式捕获,其它变量以复制方式捕获
|
如果lambda表达式不接受参数,可以不写参数列表。如果lambda表达式没有返回语句或者只有一条返回语句,可以不写返回类型。
例:创建一个自定义比较函数的优先队列
1
2
3
|
#include <queue>
auto cmp = [](int left, int right) { return left > right; };
std::priority_queue<int, std::vector<int>, decltype(cmp)> q(cmp);
|
例:递归函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
std::vector<std::vector<int>> mat(M, std::vector<int>(N)); // MxN的矩阵
std::function<int(int, int)> f;
f = [&f, &mat, M, N](int i, int j) -> int {
int length = 0;
if (i > 0 && mat[i - 1][j] > mat[i][j])
length = std::max(length, f(i - 1, j));
if (i < M && mat[i + 1][j] > mat[i][j])
length = std::max(length, f(i + 1, j));
if (j > 0 && mat[i][j - 1] > mat[i][j])
length = std::max(length, f(i, j - 1));
if (j < N && mat[i][j + 1] > mat[i][j])
length = std::max(length, f(i, j + 1));
return length + 1;
};
int max_length = 0;
for (int i = 0; i < M; ++i) {
for (int j = 0; j < N; ++j)
max_length = std::max(max_length, f(i, j));
}
|
需要注意,lambda表达式的类型是闭包类型(ClosureType
),不是std::function
,因此不能赋值:
1
2
|
auto cmp = [](int left, int right) { return left > right; };
cmp = [](int left, int right) { return left < right; }; // error
|
柯里化(currying)是将接受多个参数和函数变换成接受一个参数,而返回接受剩余参数的新函数的技术。通俗理解就是对参数列表进行切片:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
int add(int a, int b, int c) {
return a + b + c;
}
std::function<int(int, int)> curried_add(int a) {
return [a](int b, int c) { return a + b + c; };
}
// curried_add只接受一个参数,但需要调用两次
int c = curried_add(1)(2, 3); // 6
// 也可以固定某些参数,生成新的函数
auto add1 = curried_add(1);
int c = add1(2, 3); // 6
|
在许多时候,我们不想写上面的curried_add这样的函数,而是希望直接将函数的部分参数固定,以得到像add1这样的函数。c++11之前使用bind1st和bind2nd来绑定一个二元函数的参数,c++11之后使用bind来绑定任意函数的任意多个参数,对于未绑定的那些参数,使用占位符替换。
bind声明于头文件<functional>
,声明为:
1
2
|
template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );
|
使用方法为:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include <functional>
int add(int a, int b, int c) {
return a + b + c;
}
auto add1 = std::bind(add, 1, std::placeholders::_1, std::placeholders::_2);
auto add2 = std::bind(add, std::placeholders::_1, 2, std::placeholders::_2);
auto add3 = std::bind(add, std::placeholders::_1, std::placeholders::_2, 3);
add1(2, 3); // 6
add2(1, 3); // 6
add3(1, 2); // 6
|
通过使用相同的占位符,还可以将两个参数绑定在一起却又不固定:
1
2
|
auto add1 = std::bind(add, 1, std::placeholders::_1, std::placeholders::_1);
add1(2); // 1+2+2=5
|
需要注意,bind的返回类型是实现定义的,不是std::function,但可以转换为std::function:
1
2
3
4
|
std::function<int(int,int)> add1 = std::bind(add, 1, std::placeholders::_1,
std::placeholders::_2);
std::function<int(int)> add2 = std::bind(add, 1, std::placeholders::_1,
std::placeholders::_1);
|
例:生成随机数
1
2
3
4
5
|
std::function<int(void)> random_function = std::bind(
std::uniform_int_distribution<int>(0, 10),
std::default_random_engine{std::random_device{}()});
for (int i = 0; i < 10; ++i)
A[i] = random_function();
|
与OpenMP类似的框架还有微软的PPL(Parallel Patterns Library)和英特尔的TBB(Thread Building Blocks),PPL和TBB以库的方式提供,不需要其它厂商的额外支持。PPL只能支持visual studio,而TBB是开源软件,可以支持大多数常见的平台和编译器,此处简单介绍TBB库。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// 文件:vector_add_tbb.cpp
#include <iostream>
#include <vector>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
int main() {
const int n = 10;
std::vector<int> v1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> v2 = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
std::vector<int> v3(n);
tbb::parallel_for(tbb::blocked_range<int>(0, n),
[&v1, &v2, &v3](const tbb::blocked_range<int> &range) {
for (int i = range.begin(); i != range.end(); ++i)
v3[i] = v1[i] + v2[i];
}
);
for (int i = 0; i < n; ++i)
std::cout << v3[i] << " ";
std::cout << std::endl;
return 0;
}
|
编译命令:g++ -std=c++11 vector_add_tbb.cpp -o vector_add_tbb -ltbb
执行:
1
2
|
$ ./vector_add_tbb
11 11 11 11 11 11 11 11 11 11
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// 文件:vector_dot_tbb.cpp
#include <iostream>
#include <vector>
#include <tbb/blocked_range.h>
#include <tbb/parallel_reduce.h>
int main() {
const int n = 10;
std::vector<int> v1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> v2 = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
int sum = tbb::parallel_reduce(tbb::blocked_range<int>(0, n), 0,
[&v1, &v2](const tbb::blocked_range<int> &range, int s) -> int {
for (int i = range.begin(); i != range.end(); ++i)
s += v1[i] * v2[i];
return s;
},
[](int s1, int s2) -> int { return s1 + s2; }
);
std::cout << sum << std::endl;
return 0;
}
|
编译:g++ -std=c++11 vector_dot_tbb.cpp -o vector_dot_tbb -ltbb
执行:
1
2
|
$ ./vector_dot_tbb
220
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <limits>
#include <functional>
#include <tbb/blocked_range.h>
#include <tbb/parallel_reduce.h>
std::ostream &operator<<(std::ostream &out, const std::vector<int> &vec) {
if (vec.empty())
return out;
for (int i = 0; i + 1 < vec.size(); ++i)
out << vec[i] << ", ";
return out << vec.back();
}
int main() {
const int N = 16;
std::vector<int> nums(N);
std::function<int(void)> random_function = std::bind(
std::uniform_int_distribution<int>(0, 100),
std::default_random_engine{std::random_device{}()});
std::generate(nums.begin(), nums.end(), random_function);
std::cout << "numbers: " << nums << std::endl;
int max_value = tbb::parallel_reduce(
tbb::blocked_range<int>(0, nums.size()),
std::numeric_limits<int>::min(),
[&nums](const tbb::blocked_range<int> &range, int max_value) -> int {
for (int i = range.begin(); i != range.end(); ++i)
max_value = std::max(max_value, nums[i]);
return max_value;
},
[](int a, int b) -> int {
return a > b ? a : b;
}
);
std::cout << "max value: " << max_value << std::endl;
return 0;
}
|
执行结果:
1
2
|
numbers: 50, 36, 20, 81, 98, 55, 62, 98, 39, 2, 1, 26, 45, 25, 48, 93
max value: 98
|
TBB比OpenMP的抽象层次更高,更适合c++程序设计,除parallel_for和parallel_reduce以外,还提供了parallel_pipline、parallel_sort、parallel_scan、concurrent_unordered_map、concurrent_queue和concurrent_vector等并行算法或并发容器,可以大大节省程序员的精力。对于一些精细控制的任务,TBB不合适的,可以搭配OpenMP使用。
[1] TBB文档:https://software.intel.com/content/www/us/en/develop/documentation/tbb-documentation/top.html.