转载自痴痴笑笑的博客,略有删改。
尽管 C++ 社区对 C++ 0x 很是追捧,但是各厂商对于新标准的支持并不热乎。盼星星盼月亮,微软作为 Windows 平台上最强势的 C++ 编译器厂商也终于在 Visual Studio 2010 中开始支持 C++ 0x 的特性。
Visual Studio 2010 中的 Visual C++ 编译器,即 VC10, 包含了 4 个 C++ 0x 的语言特性:lambda 表达式,自动类型推演(auto 关键字),静态断言(static_assert)和右值引用(rvalue reference)。
Lambda 表达式
使用过函数式编程语言(如 LISP、 F#)或一些动态语言(如 Python、Javascript)的大侠对于 lambda 表达式一定不会陌生。在 C++ 0x 中,引入了 lambda 表达式来定义无名仿函数。下面是一个 lambda 表达式的简单例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
cout << endl;
return 0;
}
|
运行结果如下:
for_each 一行中,中括号 [] 称为 lambda introducer,它告诉编译器接下来的是一个 lambda 表达式;接下来 (int n) 是 lambda 表达式的参数声明;最后大括号里边就是“函数体”了。
注意这里因为 lambda 表达式生成的是 functor,所以“函数体”实际上是指这个 functor 的 operator() 的调用部分。你也许会问:那么返回值呢?缺省情况下 lambda 表达式生成的 functor 调用返回类型为 void。
为了方便,以下会用“lambda 返回 void”的简短表述来代替冗长啰嗦的表述—— lambda 表达式生成一个 functor 类型,这个 functor 类型的函数调用操作符(operator()),返回的类型是 void。
请大家一定记住:lambda 表达式生成了类型,并构造该类型的实例。
下面的例子中 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
|
#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
for_each(v.begin(), v.end(), [](int n) {
cout << n;
if (n % 2 == 0) {
cout << " even ";
} else {
cout << " odd ";
}
});
cout << endl;
return 0;
}
|
上文提到了 lambda 表达式缺省情况下返回 void,那么如果需要返回其他类型呢?答案是:lambda 表达式的“函数体”中如果有一个 return 的表达式,例如 { return expression; },那么编译器将自动推演 expression 的类型作为返回类型。
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
|
#include <algorithm>
#include <deque>
#include <iostream>
#include <iterator>
#include <ostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
deque<int> d;
transform(v.begin(), v.end(), front_inserter(d), [](int n) { return n * n * n; });
for_each(d.begin(), d.end(), [](int n) { cout << n << " "; });
cout << endl;
return 0;
}
|
上例中返回值 n * n * n 很简单,类型推演是显而易见的。但是如果 lambda 表达式中有非常复杂的表达式时,编译器可能无法推演出其类型,或者是推演出现二义性,这时候你可以显式地指明返回值类型。如下所示:
1
2
3
4
5
6
7
|
transform(v.begin(), v.end(), front_inserter(d), [](int n) -> double {
if (n % 2 == 0) {
return n * n * n;
} else {
return n / 2.0;
}
});
|
“-> double”显式地指明了 lambda 表达式的返回类型是 double。
以上例子中的 lambda 都是无状态的,不包含任何数据成员。很多时候我们需要 lambda 包含数据成员以保存状态,这一点可以通过“捕获”局部变量来实现。
Lambda 表达式的导入符(lambda introducer)是空的,也就是“[]”,表明该 lambda 是一个无状态的。但是在 lambda导入符中可以指定一个“捕获列表”,下面的代码中的 lambda 使用了局部变量 x 和 y,将值介于 x 和 y 之间的元素从集合中删除:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
int main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
int x = 0;
int y = 0;
cout << "Input: ";
cin >> x >> y;
v.erase(remove_if(v.begin(), v.end(), [x, y](int n) { return x < n && n < y; }), v.end());
for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
cout << endl;
return 0;
}
|
运行结果如下:
Input: 4 7
0 1 2 3 4 7 8 9
上面代码中很重要的一点信息是:lambda 中捕获的局部变量是以“传值”的方式传给匿名函数对象的。在匿名函数对象中,保存有“捕获列表”中局部变量的拷贝。这一点使得匿名函数对象的生命周期能够长于 main 中的 x、y 局部变量。然而这样的传值方式带来几个限制:
- lambda中的这两个拷贝并不能被改变,因为缺省情况下函数对象的 operator() 是const
- 有的对象的拷贝操作开销很大或者不可能(例如如果上面代码中的 x、y 是数据库链接或者某个 singleton)
- 即使在lambda内部修改了 m_a、m_b 也不能够影响外边main函数中的 x 和 y
既然有了“传值”,你一定猜到了还会有“传引用”。Bingo! 你是对的。
在讨论“传引用”之前,我们先来看看另一个比较有用的东西。假设你有一大堆的局部变量需要被 lambda 使用,那么你的“捕获列表”将会写的很长,这肯定不是件愉快的事情。
好在 C++ 委员会的老头们也想到了,C++ 0x 中提供了一个省心的东西:如果捕获列表写成 [=],表示 lambda 将捕获所有的局部变量,当然也是传值方式。这种方式姑且被称为“缺省捕获”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
int main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
int x = 0;
int y = 0;
cout << "Input: ";
cin >> x >> y; // EVIL!
v.erase(remove_if(v.begin(), v.end(), [=](int n) { return x < n && n < y; }), v.end());
for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
cout << endl;
return 0;
}
|
当编译器在 lambda 的作用范围内看到局部变量 x、y 时,它会以传值的方式从 main 函数中将它们捕获。
下面我们来看如何突破前面提到的 3 点限制。
第 1 点,修改 lambda 表达式中的局部变量拷贝(e.g. m_a, m_b)。缺省情况下,lambda 的 operator() 是 const 修饰的,但是你可以使用 mutable 关键字改变这一点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
int main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
int x = 1;
int y = 1;
for_each(v.begin(), v.end(), [=](int& r) mutable {
const int old = r;
r *= x * y;
x = y;
y = old;
});
for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
cout << endl;
cout << x << ", " << y << endl;
return 0;
}
|
运行结果如下:
0 0 0 6 24 60 120 210 336 504
1, 1
这里我们解决了第 1 个限制,但是却产生了一个新的限制:
- lambda 中对捕获变量的修改并不会影响到 main 函数中的局部变量,因为 lambda 捕获局部变量使用的是传值方式
下面该“传引用”的方式登场了,它能够有效地解决2,3,4三个限制。传引用的语法为: lambda-introducer [&x, &y],这里的捕获列表应该理解为:X& x, Y& y,因为我们实际上是取的 x、y 的引用而不是地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
int main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
int x = 1;
int y = 1;
for_each(v.begin(), v.end(), [&x, &y](int& r) {
const int old = r;
r *= x * y;
x = y;
y = old;
});
for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
cout << endl;
cout << x << ", " << y << endl;
return 0;
}
|
运行结果如下:
0 0 0 6 24 60 120 210 336 504
8, 9
注意:当你使用 lambda 时,VC10 编译器会为 lambda 的定义部分自动禁用 C4512 警告。
当以传引用方式捕获局部变量时,lambda 的函数对象在自己内部以引用方式保存 main 函数中的局部变量。当然因为使用的是局部对象的引用,使用lambda表达式时一定要注意不能够超出局部变量的生命周期。
和上文提高的[=]类似,我们可以用[&]来以“传引用”的方式捕获所有的局部变量。
到目前为止,局部变量的捕获方式要么是“值语义”要么是“引用语义”,那么可以混合这两种方式吗?可以!例如:[a, b, c, &d, e, &f, g],其中变量 d 和 f 是按引用语义捕获,而 a、b、c、e 和 g 是按值语义捕获。
另外很有用的一点是:你可以指定一个缺省捕获,然后重载某些局部变量的捕获方式。下边例子中[=, &sum, &product] 告诉编译器用值语义方式捕获所有的局部变量,但是有两个例外 - sum和product是按引用语义来捕获。
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 main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
int sum = 0;
int product = 1;
int x = 1;
int y = 1;
for_each(v.begin(), v.end(), [=, &sum, &product](int& r) mutable {
sum += r;
if (r != 0) {
product *= r;
}
const int old = r;
r *= x * y;
x = y;
y = old;
});
for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
cout << endl;
cout << "sum: " << sum << ", product: " << product << endl;
cout << "x: " << x << ", y: " << y << endl;
return 0;
}
|
运行结果如下:
0 0 0 6 24 60 120 210 336 504
sum: 45, product: 362880
x: 1, y: 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
|
class Kitty {
public:
explicit Kitty(int toys) : m_toys(toys) {}
void meow(const vector<int>& v) const {
for_each(v.begin(), v.end(), [m_toys](int n) {
cout << "If you gave me " << n << " toys, I would have " << n + m_toys << " toys total." << endl;
});
}
private:
int m_toys;
};
int main() {
vector<int> v;
for (int i = 0; i < 3; ++i) {
v.push_back(i);
}
Kitty k(5);
k.meow(v);
return 0;
}
|
不幸的是,编译这段代码将产生这样的错误:
error C3480: 'Kitty::m_toys': a lambda capture variable must be from an enclosing function scope
为什么呢?lambda表达式能够让你不活局部变量,但是类的数据成员并不是局部变量。解决方案呢?别着急。lambda 为捕获类的数据成员大开方便之门,你可以捕获this指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class Kitty {
public:
explicit Kitty(int toys) : m_toys(toys) {}
void meow(const vector<int>& v) const {
for_each(v.begin(), v.end(), [this](int n) {
cout << "If you gave me " << n << " toys, I would have " << n + m_toys << " toys total." << endl;
});
}
private:
int m_toys;
};
int main() {
vector<int> v;
for (int i = 0; i < 3; ++i) {
v.push_back(i);
}
Kitty k(5);
k.meow(v);
return 0;
}
|
运行结果如下:
If you gave me 0 toys, I would have 5 toys total.
If you gave me 1 toys, I would have 6 toys total.
If you gave me 2 toys, I would have 7 toys total.
当 lambda 表达式捕获“this”时,编译器看到 m_toys 后会在 this 所指向对象的范围内进行名字查找,m_toys 被隐式地推演为 this->m_toys。当然你也可以让编译器省省力气,显式地在捕获列表中使用 this->m_toys。另外,lambda 比较智能,你也可以隐式地捕获 this 指针,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class Kitty {
public:
explicit Kitty(int toys) : m_toys(toys) { }
void meow(const vector<int>& v) const {
for_each(v.begin(), v.end(), [=](int n) {
cout << "If you gave me " << n << " toys, I would have " << n + m_toys << " toys total." << endl;
});
}
private:
int m_toys;
};
int main() {
vector<int> v;
for (int i = 0; i < 3; ++i) {
v.push_back(i);
}
Kitty k(5);
k.meow(v);
}
|
运行结果如下:
If you gave me 0 toys, I would have 5 toys total.
If you gave me 1 toys, I would have 6 toys total.
If you gave me 2 toys, I would have 7 toys total.
注意你也可以在上面代码中用 [&],但是结果是一样的——this 指针永远是按值语义被传递(捕获)的。你也不能够使用 [&this],呵呵。
如果你的 lambda 表达式是没有参数的,那么 lambda 表达式的导入符后边的括号()也可以省掉。例如:
1
2
3
4
5
6
7
8
|
int main() {
vector<int> v;
int i = 0;
generate_n(back_inserter(v), 10, [&] { return i++; });
for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
cout << endl;
cout << "i: " << i << endl;
}
|
上边是 [&]() { return i++; }的简写形式,个人认为省掉括号并不是什么好的 coding style。如果你需要用到mutable或者指定lambda的返回类型,空的括号就不能够省略了。
最后,既然 lambda 表达式生成是普通的函数对象,所以函数对象支持的用法 lambda 都支持。例如和 tr1 的 function 一起使用,看看下边的代码,是不是很酷?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
using namespace std;
using namespace std::tr1;
void meow(const vector<int>& v, const function<void (int)>& f) {
for_each(v.begin(), v.end(), f);
cout << endl;
}
int main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
meow(v, [](int n) { cout << n << " "; });
meow(v, [](int n) { cout << n * n << " "; });
function<void (int)> g = [](int n) { cout << n * n * n << " "; };
meow(v, g);
return 0;
}
|
运行结果:
0 1 2 3 4 5 6 7 8 9
0 1 4 9 16 25 36 49 64 81
0 1 8 27 64 125 216 343 512 729
auto 关键字
auto 这个关键字来自 C++ 98 标准。在 C++ 98 中它没有什么作用,C++ 0x 中“借用”它来作为自动类型推演(automatic type deduction)。当 auto 出现在声明中时,它表示“请用初始化我的表达式类型作为我的类型”,例如下面代码:
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
|
#include <iostream>
#include <map>
#include <ostream>
#include <regex>
#include <string>
using namespace std;
using namespace std::tr1;
int main() {
map<string, string> m;
const regex r("(\\w+) (\\w+)");
for (string s; getline(cin, s); ) {
smatch results;
if (regex_match(s, results, r)) {
m[results[1]] = results[2];
}
}
for (auto i = m.begin(); i != m.end(); ++i) {
cout << i->second << " are " << i->first << endl;
}
return 0;
}
|
运行结果如下:
cute kittens
ugly puppies
evil goblins
^Z
kittens are cute
goblins are evil
puppies are ugly
上面例子中i的类型在编译时推演为 map::iterator, 有了 auto 关键字你再也不用写又长又烦的代码了。(注意 m.begin() 返回类型是 iterator, 而不是 const_iterator, 因为这里的 m 并不是 const。C++0x 中的 cbegin() 能够解决这个问题,它返回 non-const 容器的 const 迭代器。)
Lambda 表达式和 auto 关键字的配合
上文中提到了用 tr1::functions 来存储 lambda 表达式,但是不建议那样做除非不得已,因为 tr1::functions 的开销问题。如果你需要复用 lambda 表达式或者像给它命名,那么 auto 是更好的选择。
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
|
#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;
template <typename T, typename Predicate> void keep_if(vector<T>& v, Predicate pred) {
auto notpred = [&](const T& t) {
return !pred(t);
};
v.erase(remove_if(v.begin(), v.end(), notpred), v.end());
}
template <typename Container> void print(const Container& c) {
for_each(c.begin(), c.end(), [](const typename Container::value_type& e) { cout << e << " "; });
cout << endl;
}
int main() {
vector<int> a;
for (int i = 0; i < 100; ++i) {
a.push_back(i);
}
vector<int> b;
for (int i = 100; i < 200; ++i) {
b.push_back(i);
}
auto prime = [](const int n) -> bool {
if (n < 2) {
return false;
}
for (int i = 2; i <= n / i; ++i) {
if (n % i == 0) {
return false;
}
}
return true;
};
keep_if(a, prime);
keep_if(b, prime);
print(a);
print(b);
return 0;
}
|
运行结果如下:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199
上面代码中 notpred 是一个 lambda 表达式的否定式。这个例子中我们不能够使用 C++ 98 的 not1(),因为 not1 要求你的谓词是从 unary_function 派生的,但是 lambda 并不要求这点,所以很多情况下使用 lambda 更灵活。
静态断言 static_assert
断言(assertion)是提高代码质量的有效武器。C++标准库中的 assert、MFC 中的 ASSERT /VERIFY 宏都是断言的例子,它们的共同点是在运行时对程序状态进行判断,例如检查函数的参数有效性、检查类的不变式等。而 C++ 0x 中的静态断言呢,和运行时的断言不一样,它是编译时执行检查的。看下面的例子:
1
2
3
4
5
6
7
8
9
10
|
template <int N> struct Kitten {
static_assert(N < 2, "Kitten<N> requires N < 2.");
};
int main() {
Kitten<1> peppermint;
Kitten<3> jazz;
return 0;
}
|
编译结果如下:
staticfluffykitten.cpp(2) : error C2338: Kitten<N> requires N < 2.
staticfluffykitten.cpp(8) : see reference to class template instantiation 'Kitten<N>' being compiled
with
[
N=3
]
上面例子中用 static_assert 对模板参数 N 进行了检查,如果断言失败编译器将使用用户自定义的错误消息。
转自:http://wutiam.net/2010/06/lambdas-auto-and-static-assert-c-0x-features-in-vc10/