作者 主題: 關於 c++ 11 的 lambda  (閱讀 2925 次)

0 會員 與 1 訪客 正在閱讀本文。

Yamaka

  • 俺是博士!
  • *****
  • 文章數: 4913
    • 檢視個人資料
    • http://www.ecmagic.com
關於 c++ 11 的 lambda
« 於: 2013-12-19 17:34 »
前幾天被朋友問到 c++ 的 lambda 到底要怎麼用
老實說,10多年來都在寫 web 的東西,c++ 也有10多年沒真的在用
網路到處爬了好一陣子,大概了解 c++ lambda 是怎麼一回事了

簡單說就是隱函數或匿名函數,也就是沒有函數名的函數
如果寫過 javascript 應該就很清楚(PHP v5.3 也開始支援匿名函數)

代碼: [選擇]
<script>
setTimeout(function() {
  alert("Hi!")},
  2000
); // show Hi!
</script>

第一個參數原本應該給一個函數名,而這裡就是用了所謂的匿名函數
這種用法,感覺有點像是 c/c++ 的 inline XD


在 c++ 11 也正式開始提供這種語法(特性),網路上找到很多說明大多是
用在處理集合物件,例如..

代碼: [選擇]
const int size = 5;
int array[size] = {1, 3, 5, 7, 9};

for_each(array, array + size, [](int i) { cout << i * i << endl; });


[](int i) { cout << i * i << endl; }

這東西就是 c++ 11 lambda 的新語法

代碼: [選擇]
[](){};
這是基本樣式

[] => 函數開頭,設定帶入可視範圍變數的模式,大概有這幾種用法
      1. []: 省略,不帶入任何變數
      2. [=]: 全部以傳值方式帶入變數
      3. [&]: 全部以參考方式帶入變數
      4. [var1]: (或是 &var1)指定帶入的變數,忽略其他變數
      5. [=, &var1]: 指定 var1 為參考方式,其他則傳值方式
      6. [&, var1]: 指定 var1 為傳值方式,其他則參考方式
      7. [this]: 傳物件的 this 指標
      (下面會有使用範例)

() => 函數的參數,可以省略
{} => 函數的主體,可以省略

三種括號內都可以省略,所以

代碼: [選擇]
[](){};
是一個合法的匿名函數,只是什麼事也不做


上面那個 for_each 範例在執行時,會自動將 array 內容依序丟給後面匿名函數當做參數
也就是 i, 然後在函數裡印出 i * i



-----
其實我比較想要知道的是一般用法,例如像是 javascript 裡這樣的用法

代碼: [選擇]
<script>
var fVar = function(arg) {
  return arg % 5;
};

alert(fVar(9)); //show 4
</script>

或是上面 setTimeout 的裡用法,下面整理了幾種方式,
其實都差不多,只是用的場合不同

(1)
$ cat lambda01.cc
代碼: [選擇]
#include <iostream>
using namespace std;

int main() {
  int a = 5, b = 6;
  auto fn = [&](int c) { a++; b--; return a + b + c; };
 
  cout << "fn(3): " << fn(3) << endl;
  cout << "a: " << a << endl;
  cout << "b: " << b << endl;

  return 0;
}

環境是 ubuntu 12.04 g++ 4.6.3,編譯時要加 -std=c++0x
g++ 4.7.x 才正式支援 c++11 標準,所以 g++ 4.7 以後編譯時就可以直接用 -std=c++11

$ g++ -std=c++0x -o lambda01 lambda01.cc
$ ./lambda01
fn(3): 14
a: 6
b: 5

因為使用 [&], a, b 變數會以參考的方式被帶入函數,因此在函數裡可以修改 a, b 的值


(2)
$ cat lambda02.cc
代碼: [選擇]
#include <iostream>
#include <string>
using namespace std;

int main() {
  string str1, str2;
  str1 = "Hello ";
  str2 = "World!";
 
  auto sn = [=](string str3){ cout << str1 << str2 << str3 << endl; };
 
  str1 = "『Hello ";
  str2 = "World!』";
 
  sn(" Yo~~");
  return 0;
}

$ g++ -std=c++0x -o lambda02 lambda02.cc
$ ./lambda02
Hello World! Yo~~

因為是 [=],函數內不能修改 str1, str2 內容,而且 str1, str2 值會被鎖定在函數之前的值
所以,雖然在函數之後 str1, str2 內容被改了,sn() 仍然會印出原來的值
如果改用 [&], 結果會怎樣呢? ^_^


(3)
$ cat lambda03.cc
代碼: [選擇]
#include <iostream>
using namespace std;

int main() {
  [](){ cout << "我是合法的 lambda 函數!" << endl; };
 
  cout << [](){ int x = 9; return x * x; }() << endl;
 
  cout << [](int x){ return x * x; }(12) << endl;
 
  int a = 20;
  cout << [](int &x) { x++; return x * x; }(a) << endl;
  cout << "a: " << a << endl;
  return 0;
}

$ g++ -std=c++0x -o lambda03 lambda03.cc
$ ./lambda03
81
144
441
a: 21

(1)(2)兩種用法是先將匿名函數指定給一個變數,然後透過這個變數來呼叫函數
(3)則是連指定給變數的動作都省了,就類似 for_each 裡的用法
第 2,3 行大括號後面的小括號是必要的,如果沒有那對小括號,則函數只是被定義在那裡而已
實際上這個函數在執行時並沒有被呼叫,就像第 1 行,因為後面沒接 (), 因此沒有印出那串字
要在定義函數後立即呼叫執行的話,最後面要加(), 傳入的參數也是要放在這個 () 裡
將第 1 行成

代碼: [選擇]
[](){ cout << "我是合法的 lambda 函數!" << endl; }();
$ g++ -std=c++0x -o lambda03 lambda03.cc
$ ./lambda03
我是合法的 lambda 函數!
81
144
441
a: 21


(4)
$ cat lambda04.cc
代碼: [選擇]
#include <iostream>
using namespace std;

int main() {
  int x = 31;
  int b = 5;
 
  if (x > [b](int x){ cout << "(x:" << x << ")" << endl; return x * b; }(6)) {
    cout << "Yes [ x:" << x << ", b:" << b << " ]" << endl;
  } else {
    cout << "No [ x:" << x << ", b:" << b << " ]" << endl;
  }
 
  b = 6;
  cout << ((x > [b](int x){ return x * b; }(6)) ? "yes" : "no") << endl;
 
  return 0;
}

$ g++ -std=c++0x -o lambda04 lambda04.cc
$ ./lambda04
(x:6)
Yes [ x:31, b:5 ]
no

這個例子是將匿名函數放在判斷式裡,所以 x 實際上是跟匿名函數的傳回值做比較
而且在匿名函數還可以用 cout 印出資料來 XD
既然可以放在判斷式裡,那也就是說,應該也可以用在迴圈的判斷式吧?


(5)
$ cat lambda05.cc
代碼: [選擇]
#include <iostream>
using namespace std;

int main() {
  int b = 10;
  while ([&b](){ return b--; }()) {
    cout << b << ' ';
  }
  cout << endl << "b: " << b << endl;
 
  b = 10;
  while ([](int &x){ return x--; }(b)) {
    cout << b << ' ';
  }
  cout << endl << "b: " << b << endl;
 
  return 0;
}

$ g++ -std=c++0x -o lambda05 lambda05.cc
$ ./lambda05
9 8 7 6 5 4 3 2 1 0
b: -1
9 8 7 6 5 4 3 2 1 0
b: -1

兩個 while 迴圈執行結果一樣,第 1 個直接以參考方式抓外面變數 b 來用
第 2 個則是以傳統方式將 b 以參考方式傳入
不過匿名函數不建議用在這種情形,一不小心搞錯變數就形成無窮迴圈了


(6)
$ cat lambda06.cc
代碼: [選擇]
#include <iostream>
using namespace std;

int main() {
  int l = 0, m = 1, n = 2;
 
  [=, &l](){ printf("(i)l: %d, m: %d, n: %d\n", l++, m, n); }();
  printf("(o)l: %d, m: %d, n: %d\n", l, m, n);
 
  l = 0;
  m = 1;
  n = 2;
  [&, l](){ printf("(i)l: %d, m: %d, n: %d\n", l, m++, n++); }();
  printf("(o)l: %d, m: %d, n: %d\n", l, m, n);
 
  return 0;
}

(還是 printf 好用 XD)

$ g++ -std=c++0x -o lambda06 lambda06.cc
$ ./lambda06
(i)l: 0, m: 1, n: 2
(o)l: 1, m: 1, n: 2
(i)l: 0, m: 1, n: 2
(o)l: 0, m: 2, n: 3

第 1 個匿名函數除了 l 外,其他傳值帶入,
所以 l 可以做 ++, 其他變數如果也做 ++
編譯時會丟出 read-only object 的錯誤訊息。
第 2 個函數是反過來,除了 l 外,其他以參考方式帶入
同樣的,如果在第 2 個函數內對 l 動手動腳,編譯時會丟出錯誤訊息


(7)
$ cat lambda07.cc
代碼: [選擇]
#include <iostream>
using namespace std;

class MyClass {
  private:
    int _num1;

  public:
    MyClass(int num) : _num1(num) {}
    void show() {
      [this]() { cout << "_num1: " << _num1 << endl; } ();
    }
};

int main() {
  MyClass mClass(123);
  mClass.show();
  return 0;
}

$ ./lambda07
_num1: 123

[this] 只知道如何使用,還沒想到具體的應用場合
在 show() 裡的匿名函數裡用到了 _num1,如果沒設定 [this] 的話,會有錯誤

error: ‘this’ was not captured for this lambda function

-----

以上,是花了整個晚上爬網歸納結果所得,如有錯誤,請用力指正 ^_^

ps:
如果想知道自己系統環境裡的 c++ 編譯器有沒有支援 c++11 標準


代碼: [選擇]
[](){};
這行丟給他編譯看看就知道了 XD
記得要加 -std=c++0x 或 -std=c++11 喔

Yamaka

  • 俺是博士!
  • *****
  • 文章數: 4913
    • 檢視個人資料
    • http://www.ecmagic.com
Re: 關於 c++ 11 的 lambda
« 回覆 #1 於: 2013-12-20 19:58 »
在 windows 環境,據說新版 vc++, c# 都已有支援 lambda,不過我沒用過


Dev-C++ 的話 4.9.9.2 版預設 gcc 是 3.x 版,所以不支援
想在 Dev-C++ 編 lambda 可以自己抓新版 mingw 來更新 gcc
或是改用 Orwell Dev-C++, 這是 2011 年開始的 Dev-C++ 分支
最新 5.5.3 版 gcc 是 4.7.x 版


或是用另外一套也是 GPL 的 IDE Code::Blocks
最新版 gcc 也是 4.7.x 版,這個我有試過,有支援 c++11
而且 Code::Blocks 是跨平台,也可以在 linux 桌面安裝