VS2010 中的 C++ 0x 新特性:Lambdas、auto 和 static_assert

VS2010 中的 C++ 0x 新特性:Lambdas、auto 和 static_assert

转载自痴痴笑笑的博客,略有删改。

尽管 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 局部变量。然而这样的传值方式带来几个限制:

  1. lambda中的这两个拷贝并不能被改变,因为缺省情况下函数对象的 operator() 是const
  2. 有的对象的拷贝操作开销很大或者不可能(例如如果上面代码中的 x、y 是数据库链接或者某个 singleton)
  3. 即使在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 个限制,但是却产生了一个新的限制:

  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/

你可能感兴趣的:(VS2010 中的 C++ 0x 新特性:Lambdas、auto 和 static_assert)