在〈一級函式與 algorithm〉,看了幾個 algorithm 的函式可以接受函式的例子,既然如此,函式也可以傳回函式,這邊的指的函式傳遞,包括了函式指標、lambda 運算式。
從函式中傳回函式指標,基本上沒什麼問題,因為函式指標不會消失,然而,從函式中傳回 lambda 運算式,就得留意一下了,因為函式中的 lambda 運算式,生命周期就是侷限於函式之中,如果如下傳回函式:
#include <iostream>
using namespace std;
auto foo() {
auto f = [] { cout << "foo" << endl; };
return f;
}
int main() {
auto fn = foo();
fn();
return 0;
}
那麼沒什麼問題,f 會複製給 fn,然而如果是傳回參考:
auto& foo() {
auto f = [] { cout << "foo" << endl; };
return f;
}
因為 foo 函式執行過後,呼叫者參考的 f 變數已經無效,編譯時就會產生警訊:
warning: reference to local variable 'f'
另一個問題是,若以參考方式捕捉了區域變數:
auto foo() {
string text = "foo";
auto f = [&] { cout << text << endl; };
return f;
}
編譯雖然會過,然而實際上捕捉的變數在 foo 函式執行過後已經無效,最後呼叫傳回的 lambda 運算式時,就會發生不可預期的結果,如果你是從其他具有一級函式特性的語言來到 C++,要記得的就是,C++ 的 lambda 運算式,並不會擴展被捕捉變數的生命周期。
這並不是指傳回 lambda 運算式時,就不能用 & 來捕捉變數,主要還是要看捕捉的變數,其位址是否有效,例如以下就沒有問題,因為實際上 lambda 運算式捕捉的變數,參考的位址是 main 中的 text 變數位址:
#include <iostream>
#include <string>
using namespace std;
auto foo(string &text) {
auto f = [&] { cout << text << endl; };
return f;
}
int main() {
string text = "foo";
auto f = foo(text);
f();
return 0;
}
來看看傳回 lambda 運算式的一個例子,感覺像是函式產生了新函式,記憶了指定的引數:
#include <iostream>
using namespace std;
auto add(int m) {
return [m] (int n) { return m + n ; };
}
int main() {
auto plus10 = add(10);
cout << plus10(20) << endl; // 30
cout << plus10(40) << endl; // 50
return 0;
}
那麼可不可以接受函式、傳回函式呢?
#include <iostream>
using namespace std;
int binary_fun(int, int);
int add(int m, int n) {
return m + n;
}
int mul(int m, int n) {
return m * n;
}
auto bind(decltype(binary_fun) bf, int fst) {
return [bf, fst] (int snd) { return bf(fst, snd); };
}
int main() {
auto add10 = bind(add, 10);
auto mul5 = bind(mul, 5);
cout << add10(30) << endl; // 40
cout << mul5(20) << endl; // 100
return 0;
}
範例的 bind 方法,可以接受函式並傳回函式,傳回的函式綁定了第一個引數,像 bind 這類可以接受函式、傳回函式的函式,稱為高階函式(high-order function)。
實際上,functional 標頭檔就提供了個 bind 可以使用,而且更有彈性,可以指定要綁定哪個參數:
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
int add(int m, int n) {
return m + n;
}
int mul(int m, int n) {
return m * n;
}
int main() {
auto add10 = bind(add, _1, 10);
auto mul5 = bind(mul, _1, 5);
cout << add10(30) << endl; // 40
cout << mul5(20) << endl; // 100
return 0;
}
佔位符 _1 是位於 std::placeholders 名稱空間之中,代表傳回的函式可接受的第一個參數,以上例來說,bind(add, _1, 10) 表示 add 的 a 會是佔位符 _1,b 會是 10,因此傳回的函式第一個參數接受到的引數,相當於指定了 a 的值。
因此若有多個參數要綁定,會是如下:
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
void foo(int a, int b, int c, int d) {
cout << "a: " << a << endl
<< "b: " << b << endl
<< "c: " << c << endl
<< "d: " << d << endl;
}
int main() {
auto wat = bind(foo, _1, 20, _2, 40);
wat(10, 30);
return 0;
}
在上例中,b 被綁定為 30,d 被綁定為 40,傳回的函式第一個引數值會是 a 的值,第二個引數值會是 c 的值,因此結果顯示如下:
a: 10
b: 20
c: 30
d: 40
因此,如果想調換參數順序,可以如下:
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
void foo(int a, int b) {
cout << "a: " << a << endl
<< "b: " << b << endl;
}
int main() {
auto wat = bind(foo, _2, _1);
wat(10, 20);
return 0;
}
執行結果如下:
a: 20
b: 10
實際上,functional 中包含了對應於運算子的函子(Functor),像是 plus、minus、multiplies 等,之後的文件會談到函子,在這邊只要先知道,它就是個類別,重載了呼叫運算子 (),建構其實例之後,可以看成是個函式。
因此,上例可以進一步修改如下:
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
int main() {
auto add10 = bind(plus<int>{}, _1, 10);
auto mul5 = bind(multiplies<int>{}, _1, 5);
cout << add10(30) << endl; // 40
cout << mul5(20) << endl; // 100
return 0;
}
bind 預設不處理參考,因此若是以下的範例:
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
void foo(int &a, const int &b) {
a++;
cout << &b << endl;
}
int main() {
int a = 10;
int b = 20;
auto wat = bind(foo, a, b);
wat();
cout << "a: " << a << endl
<< "b: " << &b << endl;
return 0;
}
執行之後,main 中的 a 值依舊是 10,而 foo 的 b 位址與 main 的 b 不同:
0x61feb0
a: 10
b: 0x61feb8
若要符合參數的參考指定,可以使用 ref 與 cref,後者的 c 代表了 const,例如:
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
void foo(int &a, const int &b) {
a++;
cout << &b << endl;
}
int main() {
int a = 10;
int b = 20;
auto wat = bind(foo, ref(a), cref(b));
wat();
cout << "a: " << a << endl
<< "b: " << &b << endl;
return 0;
}
執行之後,main 中的 a 值是 11,而 foo 的 b 位址與 main 的 b 相同:
0x61feb0
a: 11
b: 0x61feb0

