在〈一級函式與 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