天天看點

VC10和C++ 0x (1) - lambda表達式

【本文大部分内容譯自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局部變量。然而這樣的傳值方式帶來幾個限制:

  1. lambda中的這兩個拷貝并不能被改變,因為預設情況下函數對象的operator()是const;
  2. 有的對象的拷貝操作開銷很大或者不可能(例如如果上面代碼中的x, y是資料庫連結或者某個singleton)
  3. 即使在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