0%

C++11 及之上的一些新东西

读代码的时候遇到了一些新东西,以前从没见过的语句和使用方式,惊觉 C++ 标准都扩展到 C++17 了,然而我以前写 ACM 的时候甚至连 STL 都很少用。

后来倒是陆续用过 queue 和 bitset 这样的黑科技。

这里陆续补充一点新东西的学习记录吧。


std::function

std::function 类模板是一种通用的函数封装,这个模版的实例可以对任何可以调用的目标进行存储、复制、和调用操作,包括函数、lambda 表达式、绑定表达式、以及其它函数对象等。

直接看代码示例吧,这一段来自上面的 cppreference。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/* ***********************************************
MYID : Chen Fan
LANG : G++ -std=c++11
PROG : function test
************************************************ */

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <functional>

void print_num(int n)
{
std::cout << n << std::endl;
}

struct foo
{
foo(int n) : num(n) {}
void print_add(int n) const {std::cout << num + n << std::endl;}
int num;
};

struct Print_num
{
void operator()(int n) const
{
std::cout << n << std::endl;
}
};

int main()
{
std::function<void(int)> f_display = print_num;
f_display(100);

std::function<void(int)> f_display_lambda = [](int n) { print_num(n);};
f_display_lambda(200);

std::function<void()> f_display_bind = std::bind(print_num, 300);
f_display_bind();

std::function<void(const foo&, int)> f_add_display = &foo::print_add;
foo new_foo(400);
f_add_display(new_foo, 12);

std::function<void(int)> f_add_display2 = std::bind(&foo::print_add, new_foo, std::placeholders::_1);
f_add_display2(123);

std::function<void(int)> f_add_display3 = std::bind(&foo::print_add, &new_foo, std::placeholders::_1);
f_add_display3(234);

std::function<void(int)> f_display_obj = Print_num();
f_display_obj(700);

return 0;
}

Lambda 表达式

这个东西的官方定义是能构造一个闭包:是能够捕获作用域中变量的无名函数

简单地说可以看成是个匿名函数,写法就是函数的结构,只是没有名字。

详细的语法:

1
2
3
4
[ capture-list ] ( params ) mutable(可选) constexpr(可选)(C++17) exception attribute -> ret { body }
[ capture-list ] ( params ) -> ret { body }
[ capture-list ] ( params ) { body }
[ capture-list ] { body }

基本上是三个部分组成,[] 中是捕获列表,() 中是函数的输入参数(跟普通的函数是一个意思,空的就是没有输入参数),-> 后面可以跟函数返回值的类型,不加的话可以自动推断,{} 中是函数体(跟正常函数一样)。

这个结构其实跟普通的函数基本是一致的。

具体的看示例:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/* ***********************************************
MYID : Chen Fan
LANG : G++ -std=c++11
PROG : Lambda
************************************************ */

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <ctime>

int main()
{
srand(time(NULL));

std::vector<int> a(10);
/* 这里是一个 std::function 跟 Lambda 表达式的例子 */
std::function<void()> aout =
// Lambda 函数体中用到了 a 这个 vector,需要“从外界捕获到闭包内”,大致可以这么理解。
// a 前面加 & 是引用捕获,闭包内的 a 和闭包外的 a 在内存里面是一样的。
// 如果不加就是值捕获,也就是把外面的 a 在当前位置的值传进去。
// 注意当前位置这个概念很重要,用下面那个不带 & 的来测试会发现后面输出的全都是 0。
// 把这个 Lambda 表达式的定义换个位置试试就明白了。
//[a]()
[&a]()
{
for (auto i : a)
std::cout << i << std::endl;
};

//-----------
std::generate(a.begin(), a.end(),
// 一个不需要捕获也没有输入参数的函数。
[]()
{
return rand() % 100;
});
// std::function 的调用例子,函数模版加上 Lambda 表达式之后其实可以看成是个普通的函数。
aout();

int sum = 0;
std::for_each(a.begin(), a.end(),
// 需要引用捕获 sum,并且要把 vector 中每个元素作为参数传进去。
// 如果不加 & 但是又在函数体中改了捕获变量的内容会发生 read-only 错误。
[&sum](int i)
{
sum += i;
});
std::cout << "Sum: " << sum << std::endl;
//-----------
int step = 2;
int i = 0;
std::generate(a.begin(), a.end(),
// 后两个都是对的,捕获列表里面可以加多个,不同变量可以用不同的捕获方式。
// 直接写 & 则 Lambda 表达式会自动推断把函数体中出现的变量全用引用捕获抓进来。
//[i, step]()
//[&i, step]()
[&, step]()
{
i += step;
return i;
});
aout();
std::cout << "i: " << i << std::endl;

step = 3; i = 0;
std::generate(a.begin(), a.end(),
// 直接写 = 则 Lambda 表达式会自动推断把函数体中的变量用值捕获抓进来。
// mutable 的意思是让捕获过来的值引用变量在 Lambda 表达式的范围内可变。
// 但是在全局范围内无影响,因为毕竟还是值引用,相当于在 Lambda 的作用范围内加了个成员变量。
// 对比下面输出来的 i 的值就明白了。
//[i, step]() mutable
[=]() mutable
{
i += step;
return i;
});
aout();
std::cout << "i: " << i << std::endl;
//-----------
step = 4; i = 0;
std::function<int()> addstep =
[&]()
{
i += step;
return i;
};
std::generate(a.begin(), a.end(), addstep);
aout();

return 0;
}

我刚开始遇到 Lambda 表达式是在 TensorFlow 的代码里面,弄明白上面这一串之后,回到 TensorFlow 的代码里,抽出来了这样一段看上去有点诡异的代码:

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
/* ***********************************************
MYID : Chen Fan
LANG : G++ -std=c++11
PROG : special Lambda
************************************************ */

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

// Closure 是一个函数模版,表示一个返回值为空的函数
typedef std::function<void()> Closure;
// Runner 也是一个函数模版,返回值为空,输入参数是上面那个 Closure
typedef std::function<void(Closure)> Runner;

Closure closure_;
Runner runner_;

int main()
{
// closure_ 可以直接赋值成一个返回值和输入参数都为空的 Lambda 表达式
closure_ = [](){ printf("aaa\n");};
// runner_ 赋值的是一个输入参数为 Closure 的 Lambda 表达式
runner_ = [](Closure f){ f();};

// runner_ 本身是一个函数,可以直接调用,输入参数是一个 Closure
// 这里的 Closure 可以直接是一个符合 Closure 格式的 Lambda 表达式,因为 Closure 本身就是个函数模版
runner_([](){ printf(".....\n");});
// 也可以像这样输个 Closure 类型的参数进去
runner_(closure_);

return 0;
}

std::bind

bind 用于把一些参数跟一个调用进行绑定起来,包括函数对象,函数指针,引用函数,成员函数的指针,指针到成员数据等等。

主要是看代码的时候看到了个占位符,不太明白,所以记一下。

还是先上代码:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/* ***********************************************
MYID : Chen Fan
LANG : G++
PROG : Bind_test
************************************************ */

#include <random>
#include <iostream>
#include <functional>

void f(int n1, int n2, int n3, const int& n4, int n5)
{
std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << '\n';
}

int g(int n1)
{
return n1;
}

struct Foo {
void print_sum(int n1, int n2)
{
std::cout << n1+n2 << '\n';
}
int data = 10;
};

int main()
{
using namespace std::placeholders;

// demonstrates argument reordering and pass-by-reference
int n = 7;
auto f1 = std::bind(f, _2, _1, 42, std::cref(n), n);
n = 10;
f1(1, 2, 1001); // 1 is bound by _1, 2 is bound by _2, 1001 is unused
f(2, 1, 42, std::cref(n), n);
std::cout << "---" << std::endl;

// nested bind subexpressions share the placeholders
auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
f2(10, 11, 12);
f(12, g(12), 12, 4, 5);
std::cout << "---" << std::endl;

// common use case: binding a RNG with a distribution
std::default_random_engine e;
std::uniform_int_distribution<> d(0, 10);
std::function<int()> rnd = std::bind(d, e);
for(int n=0; n<10; ++n)
std::cout << rnd() << ' ';
std::cout << std::endl << "---" << std::endl;

// bind to a member function
Foo foo;
auto f3 = std::bind(&Foo::print_sum, foo, 95, _1);
f3(5);
foo.print_sum(95, 5);
std::cout << "---" << std::endl;

// bind to member data
auto f4 = std::bind(&Foo::data, _1);
std::cout << f4(foo) << '\n';
std::cout << foo.data << '\n';
}

第一段的 f1 是 f 这个函数加上一些参数的封装。_1 和 _2 都是占位符,分别表示等一下输入的第一个和第二个参数。所以后面的例子中 f1(1, 2, ...) 就等于 f(2, 1, ...),bind 封装里面没有用到第三个占位符,所以 f1 中输入的 1001 是没有用的。

第二段是说在 bind 中再加个 bind,占位符是共享最外层的那个。f2 中输入的 10 和 11 无效,因为没有用到 _1 和 _2。


To be continued.