CATEGORY / Development

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

Permanent Link: http://wutiam.net/notes/132

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

尽管 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 进行了检查,如果断言失败编译器将使用用户自定义的错误消息。

3 Comments / Trackbacks / Pingbacks

  • ffl

    Jun 14, 2010 @ 03:18 Reply / Quote / #1

    总觉得C++并不是在让语言越来越好用,而是越来越难用。
    c#和java的lambda表达式比这个好用多了。c#的var也比auto好用。
    c#的linq技术比这些不知道好用多少 倍。

    c++保持c兼容是最大的成功,同时也是最大的失败。

  • Jun 17, 2010 @ 11:29 Reply / Quote / #2

    嗯,CPP 对 C 兼容在最初推广期占了很大便宜,从目前来看,毕竟 30 多年过去了,优点变成缺点也很正常。但是一个语言标准,尤其是像 CPP 这样已经积累了数以亿计代码量的语言标准,想要抛弃一些短板已无可能,唯一能做的,是我们使用语言的人,为自己制定一套“使用标准子集”,该用哪些特性,避免使用哪些特性,从而最大化我们的生产力和代码的可调试性/可维护性。
    C# 当然是非常赞的语言,因为它是的“新”,可以按照最新的潮流设计语言标准,而且,C# 是以 Delphi 的思想为基础,在 C 语系道路上前进了一大步,这和 CPP 已经不在一个比较层次上了。但是,M$ 的 .net 战略是否能通吃所有领域,我个人持保留态度,呵呵

Leave a Reply

:) :wink: 8-O :lol: :-D 8) :-| :mrgreen: :oops: :-o :-? :( :twisted: :cry: more »