高階函式


在〈一級函式與 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) 表示 adda 會是佔位符 _1b 會是 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),像是 plusminusmultiplies 等,之後的文件會談到函子,在這邊只要先知道,它就是個類別,重載了呼叫運算子 (),建構其實例之後,可以看成是個函式。

因此,上例可以進一步修改如下:

#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,而 foob 位址與 mainb 不同:

0x61feb0
a: 10
b: 0x61feb8

若要符合參數的參考指定,可以使用 refcref,後者的 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,而 foob 位址與 mainb 相同:

0x61feb0
a: 11
b: 0x61feb0