转载自痴痴笑笑的博客,略有删改。
尽管 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 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/