尽管 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; } |
运行结果如下:
0 1 2 3 4 5 6 7 8 9
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 lambdacapture 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 5toys total.
If you gave me 1 toys, I would have 6toys total.
If you gave me 2 toys, I would have 7toys 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 5toys total.
If you gave me 1 toys, I would have 6toys total.
If you gave me 2 toys, I would have 7toys 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 4753 59 61 67 71 73 79 83 89 97
101 103 107 109 113 127 131 137 139 149151 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 templateinstantiation 'Kitten<N>' being compiled
with
[
N=3
]
上面例子中用 static_assert 对模板参数 N 进行了检查,如果断言失败编译器将使用用户自定义的错误消息。