零度の冰翻译,点击进入原文,转载请注明出处。
Visual Studio 2010(下称VS2010)中的Visual C++编译器包含了对4项(正式发布后可能更多)C++0x 特性的支持,分别为lambda表达式、auto关键字、static_assert,和右值引用(rvalue references)。此篇文章将对前三项进行详细解释,rvalue references将在后续文章中解释。
相关资料:
C++0x language feature status:
http://open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2705.html
C++0x library feature status:
http://open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2706.html
C++0x Working Draft:
http://open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2798.pdf
Lambda表达式
在C++0x中“lambda表达式”隐式的定义并构造了一个匿名的函数对象,它和普通的函数对象的行为是类似的。下面是“Hello World”的lambda:
| C:\Temp>type meow.cpp #include <algorithm> #include <iostream> #include <ostream> #include <vector> using namespace std; int main() { vector v; for (int i = 0; i < 10; ++i) { v.push_back(i); } for_each(v.begin(), v.end(), [](int n) { cout << n << " "; }); cout << endl; } C:\Temp>cl /EHsc /nologo /W4 meow.cpp > NUL && meow 0 1 2 3 4 5 6 7 8 9 |
代码中的“[]”叫做“lambda前导符”,它告诉编译器一个lambda表达式开始了。而“(int n)”是lambda表达式中的参数声明部分,它告诉编译器,这个匿名的函数对象中,函数调用运算符(“()”运算符)调用时接受的参数。最后“{ cout << n << " "; }”大括号语句作为这个匿名函数对象的函数调用符的函数体。默认情况下,匿名函数对象的返回值为void。
因此,上面的C++0x代码实质上等价于下面的C++98代码:
| C:\Temp>type meow98.cpp #include <algorithm> #include <iostream> #include <ostream> #include <vector> using namespace std; struct LambdaFunctor { void operator()(int n) const { cout << n << " "; } };
int main() { vector v; for (int i = 0; i < 10; ++i) { v.push_back(i); } for_each(v.begin(), v.end(), LambdaFunctor()); cout << endl; } C:\Temp>cl /EHsc /nologo /W4 meow98.cpp > NUL && meow98 0 1 2 3 4 5 6 7 8 9 |
现在,我不再说“那个匿名的函数对象的函数调用运算符的返回值是void”,而是直接说“这个lambda返回void”,但是,非常重要的一点是:一定要记住lambda表达式干了什么:定义了一个类(LambdaFunctor),然后构造了它的对象。
当然了,lambda的大括号表达式中,可以包含多个语句:
| C:\Temp>type multimeow.cpp #include <algorithm> #include <iostream> #include <ostream> #include <vector> using namespace std; int main() { vector 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; } C:\Temp>cl /EHsc /nologo /W4 multimeow.cpp > NUL && multimeow 0 even 1 odd 2 even 3 odd 4 even 5 odd 6 even 7 odd 8 even 9 odd |
现在,lambda也不必总是返回void,如果一个lambda的大括号表达式为{ return expression; },那么这个lambda的返回值类型将被自动推断为expression的类型:
| C:\Temp>type cubicmeow.cpp #include <algorithm> #include <deque> #include <iostream> #include <iterator> #include <ostream> #include <vector> using namespace std; int main() { vector v; for (int i = 0; i < 10; ++i) { v.push_back(i); } deque 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; } C:\Temp>cl /EHsc /nologo /W4 cubicmeow.cpp > NUL && cubicmeow 729 512 343 216 125 64 27 8 1 0 |
这里,n * n * n的类型为int,所以,lambda返回值类型为int。
有些更加复杂的lambda表达式无法做出类型推断,你需要显式的指定它:
| C:\Temp>type returnmeow.cpp #include <algorithm> #include <deque> #include <iostream> #include <iterator> #include <ostream> #include <vector> using namespace std; int main() { vector v; for (int i = 0; i < 10; ++i) { v.push_back(i); } deque d; transform(v.begin(), v.end(), front_inserter(d), [](int n) -> double { if (n % 2 == 0) { return n * n * n; } else { return n / 2.0; } }); for_each(d.begin(), d.end(), [](double x) { cout << x << " "; }); cout << endl; } C:\Temp>cl /EHsc /nologo /W4 returnmeow.cpp > NUL && returnmeow 4.5 512 3.5 216 2.5 64 1.5 8 0.5 0 |
“-> double”是可选的“lambda返回值类型从句”。为什么返回值类型声明没有像以前声明函数的时候放在前面呢?因为如果不把“[]”放在最前面的话,编译器将无法知道这是一个lambda表达式。
这里,如果你忘记了声明返回值类型,编译器将会报告错误:
C:\Temp>cl /EHsc /nologo /W4 borkedreturnmeow.cpp borkedreturnmeow.cpp borkedreturnmeow.cpp(20) : error C3499: a lambda that has been specified to have a void return type cannot return a value borkedreturnmeow.cpp(22) : error C3499: a lambda that has been specified to have a void return type cannot return a value |
上面列出的所有lambda都是无状态的:它们构造出来的匿名函数对象不包含任何数据成员。你也可以获得有状态的lambda,方法是通过“捕获”局部变量。空白的lambda前导符“[]”表示这是一个无状态的lambda,但是在“[]”内部,你可以指定一个捕获组:
| C:\Temp>type capturekittybyvalue.cpp #include <algorithm> #include <iostream> #include <ostream> #include <vector> using namespace std; int main() { vector v; for (int i = 0; i < 10; ++i) { v.push_back(i); } int x = 0; int y = 0; // op>>()不会读取输入流中最后一个回车换行符, // 这有时候会带来很多问题。我推荐 // 避免使用它,而使用非成员函数 // getline(cin, str)读取一整行, // 然后再解析它。但是为了简洁起见, // 我会继续使用 op>>(): 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; } C:\Temp>cl /EHsc /nologo /W4 capturekittybyvalue.cpp > NUL && capturekittybyvalue Input: 4 7 0 1 2 3 4 7 8 9 |
如果你忘记了写捕获组,编译器会报告错误:
C:\Temp>cl /EHsc /nologo /W4 borkedcapturekittybyvalue.cpp borkedcapturekittybyvalue.cpp borkedcapturekittybyvalue.cpp(27) : error C3493: 'x' cannot be implicitly captured as no default capture mode has been specified borkedcapturekittybyvalue.cpp(27) : error C3493: 'y' cannot be implicitly captured as no default capture mode has been specified |
(一会儿我将会介绍默认捕获)
记住,lambda表达式隐式的定义了一个匿名的函数对象,“{ return x < n && n < y; }”就是它函数调用符的函数体,所以尽管看起来大括号语句是在main()的范围内,但在概念上它是在main()函数范围之外的,lambda表达式中不能使用没有捕获的main()函数内的局部变量。
这是把上面程序等价转换后的代码:
| C:\Temp>type capturekittybyvalue98.cpp #include <algorithm> #include <iostream> #include <iterator> #include <ostream> #include <vector> using namespace std; class LambdaFunctor { public: LambdaFunctor(int a, int b) : m_a(a), m_b(b) { } bool operator()(int n) const { return m_a < n && n < m_b; } private: int m_a; int m_b; }; int main() { vector 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(), LambdaFunctor(x, y)), v.end()); copy(v.begin(), v.end(), ostream_iterator(cout, " ")); cout << endl; } C:\Temp>cl /EHsc /nologo /W4 capturekittybyvalue98.cpp > NUL && capturekittybyvalue98 Input: 4 7 0 1 2 3 4 7 8 9 |
这里,你可以清楚的看到捕获变量是按值传递的。局部变量的拷贝被存储在了函数对象中。这样,函数对象存活的时间可以比被捕获的局部变量长。但是,需要注意几点:(a)捕获的变量在lambda中不可修改,因为默认情况下生成的是const函数调用运算符;(b)有些对象的复制操作开销比较大;(c)局部变量的更新将不会影响lambda内已经被捕获过的副本(这符合通常的值传递语义)。一会儿,我将解释在需要的时候如何应对上述三种情况。
但是首先,如果你想捕获所有的局部变量,可以不必逐项列出它们,而只需告诉编译器“按值传递捕获所有变量”。实现方式是使用前导符“[=]”:
| C:\Temp>type defaultcapturekittybyvalue.cpp #include <algorithm> #include <iostream> #include <ostream> #include <vector> using namespace std; int main() { vector 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; } C:\Temp>cl /EHsc /nologo /W4 defaultcapturekittybyvalue.cpp > NUL && defaultcapturekittybyvalue Input: 4 7 0 1 2 3 4 7 8 9 |
当编译器看到lambda中的x和y,会将它们从main()中按值捕获进来。
对于情况(a)无法修改捕获的变量,如何处理呢?默认情况下,lambda的函数调用运算符是const的,但是你可以通过mutable关键字将其变为非const:
| C:\Temp>type capturekittybymutablevalue.cpp #include <algorithm> #include <iostream> #include <ostream> #include <vector> using namespace std; int main() { vector 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; } C:\Temp>cl /EHsc /nologo /W4 capturekittybymutablevalue.cpp > NUL && capturekittybymutablevalue 0 0 0 6 24 60 120 210 336 504 1, 1 |
上面代码将v中每个变量变为自身和它前两个变量相乘的结果。注意,(d)对捕获过的变量操作不会影响到原来变量本身(又是通常的值传递语义)。
如果你想对付(b),(c),(d)的话:使用引用传递。方法是在前导符中这样写捕获组:“[&x, &y]”:
| C:\Temp>type capturekittybyreference.cpp #include <algorithm> #include <iostream> #include <ostream> #include <vector> using namespace std; int main() { vector 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; } C:\Temp>cl /EHsc /nologo /W4 capturekittybyreference.cpp > NUL && capturekittybyreference 0 0 0 6 24 60 120 210 336 504 8, 9 |
注意capturekittybyreference.cpp中的变化:(1)lambda前导符“[&x, &y]”,(2)mutable消失了,(3)局部变量x, y的值最终变为了8、9,lambda中对它们的修改反映到了表达式外部。
下面是翻译后的等价代码:
| C:\Temp>type capturekittybyreference98.cpp #include <algorithm> #include <iostream> #include <iterator> #include <ostream> #include <vector> using namespace std; #pragma warning(push) #pragma warning(disable: 4512) // assignment operator could not be generated class LambdaFunctor { public: LambdaFunctor(int& a, int& b) : m_a(a), m_b(b) { } void operator()(int& r) const { const int old = r; r *= m_a * m_b; m_a = m_b; m_b = old; } private: int& m_a; int& m_b; }; #pragma warning(pop) int main() { vector v; for (int i = 0; i < 10; ++i) { v.push_back(i); } int x = 1; int y = 1; for_each(v.begin(), v.end(), LambdaFunctor(x, y)); copy(v.begin(), v.end(), ostream_iterator(cout, " ")); cout << endl; cout << x << ", " << y << endl; } C:\Temp>cl /EHsc /nologo /W4 capturekittybyreference98.cpp > NUL && capturekittybyreference98 0 0 0 6 24 60 120 210 336 504 8, 9 |
(当使用lambda的时候,编译会为lambda的定义部分自动关闭C4512号警告)
当你按照引用传递捕获局部变量时,生成的函数对象存储的是局部变量的引用,这样避免了复制,并且允许函数对象观察这些局部变量的变化甚至修改它们的值。(注意当前生成的函数调用运算符仍然是const修饰的,表示函数对象的成员变量不可更改,而引用类型本身就是不能更改的,但我们可以更改它引用的变量。函数调用运算符的常量性仍然得到保持)
当然了,如果一个lambda的函数对象的寿命长于它所引用的局部变量,程序会down掉。
再一次的,你可以使用默认捕获:“[&]”代表“按引用传递捕获所有”。
如何混合值传递捕获和引用传递捕获呢?你可以用“[a, b, c, &d, e, &f, g]”。也可以指定一个默认捕获方式,但对于特殊的变量指定不同的捕获方式:
| C:\Temp>type overridekitty.cpp #include <algorithm> #include <iostream> #include <ostream> #include <vector> using namespace std; int main() { vector 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; } C:\Temp>cl /EHsc /nologo /W4 overridekitty.cpp && overridekitty overridekitty.cpp 0 0 0 6 24 60 120 210 336 504 sum: 45, product: 362880 x: 1, y: 1 |
这里我们想按值捕获x, y(因为我们不想让内部对x, y副本的修改影响到他们本身),按引用捕获sum和product(因为我们确实想在lambda内部修改它们本身的值)。使用“[&, x, y]”前导符会导致同样的结果。
接下来,如果你想做下面的事情该怎么办?
| C:\Temp>type memberkitty.cpp #include <algorithm> #include <iostream> #include <ostream> #include <vector> using namespace std; class Kitty { public: explicit Kitty(int toys) : m_toys(toys) { } void meow(const vector& 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 v; for (int i = 0; i < 3; ++i) { v.push_back(i); } Kitty k(5); k.meow(v); } C:\Temp>cl /EHsc /nologo /W4 memberkitty.cpp memberkitty.cpp memberkitty.cpp(12) : error C3480: 'Kitty::m_toys': a lambda capture variable must be from an enclosing function scope |
lambda表达式允许捕获局部变量,但是数据成员不是局部变量。用一种特殊的方法,你可以捕获“this”:
| C:\Temp>type workingmemberkitty.cpp #include <algorithm> #include <iostream> #include <ostream> #include <vector> using namespace std; class Kitty { public: explicit Kitty(int toys) : m_toys(toys) { } void meow(const vector& 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 v; for (int i = 0; i < 3; ++i) { v.push_back(i); } Kitty k(5); k.meow(v); } C:\Temp>cl /EHsc /nologo /W4 workingmemberkitty.cpp > NUL && workingmemberkitty 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以后,m_toys就可以使用了,它隐式的表示this->m_toys,你也可以显示的说明this->m_toys。(在lambda表达式中,只有捕获了this后才可以使用它,你永远无法得到lambda表达式本身的this指针)
你也可以隐式的捕获this:
| C:\Temp>type implicitmemberkitty.cpp #include <algorithm> #include <iostream> #include <ostream> #include <vector> using namespace std; class Kitty { public: explicit Kitty(int toys) : m_toys(toys) { } void meow(const vector& 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 v; for (int i = 0; i < 3; ++i) { v.push_back(i); } Kitty k(5); k.meow(v); } C:\Temp>cl /EHsc /nologo /W4 implicitmemberkitty.cpp > NUL && implicitmemberkitty 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,可以直接省略小括号部分:
| C:\Temp>type nullarykitty.cpp #include <algorithm> #include <iostream> #include <iterator> #include <ostream> #include <vector> using namespace std; int main() { vector 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; } C:\Temp>cl /EHsc /nologo /W4 nullarykitty.cpp > NUL && nullarykitty 0 1 2 3 4 5 6 7 8 9 i: 10 |
这比“[&]() { return i++; }”少两个字符。到底这么干好不好就得看自己怎么想了。
搞笑的是,这意味这下面代码在C++0x中是有效的:
C:\Temp>type nokitty.cpp int main() { [](){}(); []{}(); } |
上面构造了俩啥也不干的lambda(一个有参数声明,一个没有),然后马上调用它们(最后的那个空的小括号)。
注意语法上可选的lambda参数声明包括:
(参数声明)[可选]mutable[可选]可能抛的异常[可选]lambda返回值类型[可选]
因此,如果要指定mutable或是 -> return type,那么空的()就不能省略。
最后,由于lambda生成的是普通的函数对象,你可以将其存储在 std::tr1::function 里面:
| C:\Temp>type tr1kitty.cpp #include <algorithm> #include <functional> #include <iostream> #include <ostream> #include <vector> using namespace std; using namespace std::tr1; void meow(const vector& v, const function& f) { for_each(v.begin(), v.end(), f); cout << endl; } int main() { vector 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 g = [](int n) { cout << n * n * n << " "; }; meow(v, g); } C:\Temp>cl /EHsc /nologo /W4 tr1kitty.cpp > NUL && tr1kitty 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++0x中,它被重新定义为类型自动推断。当使用它声明变量的时候,意思是说“把该变量的类型设置为初始化它的那个变量的类型”。
| C:\Temp>type autocat.cpp #include <iostream> #include <map> #include <ostream> #include <regex> #include <string> |
(注意m.begin()返回的是iterator,不是const_iterator,因为map本身不const,C++0x中cbegin()允许从非const容器中请求一个const_iterator。)
lambda和auto
之前提到过将lambda存储在std::tr1::function中。但是尽量不要这么做,因为std::tr1::function块头比较大开销也很大。如果你想复用一个lambda,或者只是想简单的给它命名,你可以使用auto。下面有一个非常简洁的例子:
| C:\Temp>type autolambdakitty.cpp #include <algorithm> #include <iostream> #include <ostream> #include <vector> using namespace std; template void keep_if(vector& v, Predicate pred) { auto notpred = [&](const T& t) { return !pred(t); }; v.erase(remove_if(v.begin(), v.end(), notpred), v.end()); } template void print(const Container& c) { for_each(c.begin(), c.end(), [](const typename Container::value_type& e) { cout << e << " "; }); cout << endl; } int main() { vector a; for (int i = 0; i < 100; ++i) { a.push_back(i); } vector 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); } C:\Temp>cl /EHsc /nologo /W4 autolambdakitty.cpp autolambdakitty.cpp C:\Temp>autolambdakitty 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(),它要求你写的“断定”部分从unary_function中派生,但是lambda不是。
static_assert
static_assert允许你使用自定义的错误消息引发编译错误:
| C:\Temp>type staticfluffykitten.cpp template struct Kitten { static_assert(N < 2, "Kitten requires N < 2."); }; int main() { Kitten<1> peppermint; Kitten<3> jazz; } C:\Temp>cl /EHsc /nologo /W4 staticfluffykitten.cpp staticfluffykitten.cpp staticfluffykitten.cpp(2) : error C2338: Kitten requires N < 2. staticfluffykitten.cpp(8) : see reference to class template instantiation 'Kitten' being compiled with [ N=3 ]
|
Stephan T. Lavavej
Visual C++ Libraries Developer
译著:本人纯属一时兴起将此文翻译,错误多多,有问题的同志请留言。