【本文大部分内容譯自Visual C++ Team Blog】http://blogs.msdn.com/vcblog/archive/2008/10/28/lambdas-auto-and-static-assert-c-0x-features-in-vc10-part-1.aspx
盡管C++社群對C++ 0x很是追捧,但是各廠商對于新标準的支援并不熱乎。盼星星盼月亮,微軟作為Windows平台上最強勢的C++編譯器廠商也終于在Visual Studio 2010中開始支援C++ 0x的特性。
Lambda表達式,auto 和靜态斷言(static_assert)
Visual Studio 2010中的Visual C++編譯器,即VC10, 包含了4個C++ 0x的語言特性 - lambda表達式,auto,static_assert 和 rvalue reference (右值引用).
相關連結:
- 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
lambdas
使用過函數式程式設計語言(如lisp, F#)或一些動态語言(如Python,Javascript)的大俠對于lambda表達式一定不會陌生。
在C++ 0x中,引入了lambda表達式來定義無名仿函數。下面是一個lambda表達式的簡單例子:
// File: meow.cpp
#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;
}
C:\Temp>cl /EHsc /nologo /W4 meow.cpp > NUL && meow
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.
是以,可以了解為上邊的代碼會被編譯器翻譯成如下:
#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;
struct LambdaFunctor {
void operator()(int n) const {
cout << n << " ";
}
};
int main() {
vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
for_each(v.begin(), v.end(), LambdaFunctor());
cout << endl;
return 0;
}
為了友善,以下會用"lambda傳回void"的簡短表述來代替冗長啰嗦的表述:lambda表達式生成一個functor類型,這個functor類型的函數調用操作符(operator())傳回的類型是void.
請大家一定記住:lambda表達式生成了類型,并構造該類型的執行個體。
下面的例子中lambda表達式的“函數體”包含多條語句:
#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的類型作為傳回類型。
#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;
}
上例中傳回值n * n * n很簡單,類型推演是顯而易見的。但是如果lambda表達式中有非常複雜的表達式時,編譯器可以無法推演出其類型,或者是推演出現二義性,這時候你可以
顯式地指明傳回值類型。如下所示:
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都是無狀态的(stateless),不包含任何資料成員。很多時候我們需要lambda包含資料成員以儲存狀态,這一點可以通過“捕獲”(capturing)局部變量來實作。
lambda表達式的導入符(lambda-introducer)是空的,也就是“[]”,表明該lambda是一個無狀态的。但是在lambda導入符中可以指定一個“捕獲清單”(capture-list)。
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;
}
上邊的代碼中的lambda使用了局部變量x和y,将值介于x和y之間的元素從集合中删除。
程式運作示例如下 -
Input: 4 7
0 1 2 3 4 7 8 9
上邊的代碼可以了解為:
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<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(), LambdaFunctor(x, y)), v.end());
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
cout << endl;
}
上面代碼中很重要的一點資訊是:lambda中捕獲的局部變量是以“傳值”的方式傳給匿名函數對象的。在匿名函數對象中,儲存有“捕獲清單”中局部變量的拷貝。
這一點使得匿名函數對象的生命周期能夠長于main中的x,y局部變量。然而這樣的傳值方式帶來幾個限制:
- lambda中的這兩個拷貝并不能被改變,因為預設情況下函數對象的operator()是const;
- 有的對象的拷貝操作開銷很大或者不可能(例如如果上面代碼中的x, y是資料庫連結或者某個singleton)
- 即使在lambda内部修改了m_a, m_b也不能夠影響外邊main函數中的x和y
既然有了“傳值”,你一定猜到了還會有“傳引用”。bingo! 你是對的。
在讨論“傳引用”之前,我們先來看看另一個比較有用的東西。假設你有一大堆的局部變量需要被lambda使用,那麼你的“捕獲清單”将會寫的很長,這肯定不是件愉快的事情。
好在C++委員會的老頭們也想到了,C++ 0x中提供了一個省心的東西:如果捕獲清單寫成 [=],表示lambda将捕獲所有的局部變量,當然也是傳值方式。這種方式姑且被稱為“預設捕獲”(capture-default)。
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;
}
當編譯器在lambda的作用範圍内看到局部變量x, y時,它會以傳值的方式從main函數中将他們捕獲。
下面我們來看如何突破前面提到的3點限制。
第一點,修改lambda表達式中的局部變量拷貝(e.g. m_a, m_b)
預設情況下,lambda的operator ()是const 修飾的,但是你可以使用mutable關鍵字改變這一點。
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;
}
代碼運作結果如下
0 0 0 6 24 60 120 210 336 504
1, 1
這裡我們解決了第一個限制,但是卻産生了一個新的限制
4. lambda中對捕獲變量的修改并不會影響到main函數中的局部變量,因為lambda捕獲局部變量使用的是傳值方式
下面該“傳引用”的方式登場了,它能夠有效地解決2,3,4三個限制。
傳引用的文法為: lambda-introducer [&x, &y]
這裡的捕獲清單應該了解為:X& x, Y& y ; 因為我們實際上是取的x,y的引用而不是位址。
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;
}
運作結果如下 -
0 0 0 6 24 60 120 210 336 504
8, 9
上面代碼會被編譯器“翻譯”成:
#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<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(), LambdaFunctor(x, y));
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
cout << endl;
cout << x << ", " << y << endl;
}
注意:當你使用lambda時,VC10編譯器會為lambda的定義部分自動禁用C4512警告。
當以傳引用方式捕獲局部變量時,lambda的函數對象在自己内部以引用方式儲存main函數中的局部變量。
當然因為使用的是局部對象的引用,使用lambda表達式時一定要注意不能夠超出局部變量的生命周期。
和上文提高的[=]類似,我們可以用[&]來以“傳引用”的方式捕獲所有的局部變量。
到目前為止,局部變量的捕獲方式要麼是“值語義”要麼是“引用語義”,那麼可以混合這兩種方式嗎?可以!
例如:[a, b, c, &d, e, &f, g],其中變量d和f是按引用語義捕獲,而a,b,c,e和g是按值語義捕獲。
另外很有用的一點是:你可以指定一個預設捕獲(capture-default),然後重載(override)某些局部變量的捕獲方式。
下邊例子中[=, &sum, &product]告訴編譯器用值語義方式捕獲所有的局部變量,但是有兩個例外 - sum和product是按引用語義來捕獲。
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;
}
運作結果如下 -
0 0 0 6 24 60 120 210 336 504
sum: 45, product: 362880
x: 1, y: 1
再來看看下邊的代碼 - 在lambda中使用類成員變量
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);
}
不幸的是,編譯這段代碼将産生這樣的錯誤:
error C3480: 'Kitty::m_toys': a lambda capture variable must be from an enclosing function scope
為什麼呢?lambda表達式能夠讓你不活局部變量,但是類的資料成員并不是局部變量。
解決方案呢?别着急。lambda為捕獲類的資料成員大開友善之門,你可以捕獲this指針。
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);
}
運作結果 -
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指針。如下所示:
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表達式的導入符後邊的括号()也可以省掉。例如:
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;
}
運作結果如下:
0 1 2 3 4 5 6 7 8 9
i: 10
上邊是 [&]() { return i++; }的簡寫形式。個人認為省掉括号并不是什麼好的coding style。
下面是純粹搞笑的寫法:
int main() {
[](){}();
[]{}();
}
注意lambda的文法為:
( lambda-parameter-declaration-listopt ) mutableopt exception-specificationopt lambda-return-type-clauseopt
是以如果你需要用到mutable或者指定lambda的傳回類型,空的括号就不能夠省略了。
最後盡然lambda表達式生成是普通的函數對象,是以函數對象支援的用法lambda都支援。例如和tr1的function一起使用,
看看下邊的代碼,是不是很酷?
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);
}
運作結果:
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
【THE END】
If you love him, teach him C++, for it's heaven;
If you hate him, teach him C++, for it's hell