談?wù)凜++中閉包的實(shí)現(xiàn)

python的閉包

閉包這個(gè)概念一直有所耳聞,在以前看《Java編程思想》時(shí)第一次真正接觸,當(dāng)時(shí)的理解就是類似C++11lambda表達(dá)式捕獲外部變量的東西,只不過(guò)Java5語(yǔ)法相對(duì)簡(jiǎn)陋,還得用匿名內(nèi)部類的方式來(lái)實(shí)現(xiàn)。今天看《Python核心編程 第二版》才恍然大悟,閉包的意義。

You do not want to have f2()'s stack frame around because that will keep all of f2()'s variables alive even if we are only interested in the free variables used by f3 (). Cells hold on to the free variables so that the rest of f2() can be deallocated.

相應(yīng)代碼如下

def f2():
    x = 1
    y = 2
    z = 3
    def f3():
        y = 3  # 局部變量y覆蓋了外部變量y
        print(x, y)  # 引用了外部變量x, 此時(shí)x即閉包變量
    return f3

函數(shù)f2()返回一個(gè)內(nèi)部函數(shù),之后用于可以像這樣調(diào)用內(nèi)部函數(shù)f3()

f = f2()
f()

而由于f3()被f2()包裹起來(lái)了,用戶無(wú)法通過(guò)f2()以外的方式來(lái)取得f3()

f3()  # 拋出NameError異常

閉包的高明之處在于,正如文章最初引用的原文所述,f3()需要引用f2()棧幀的變量,正常來(lái)說(shuō)f2()在調(diào)用完畢后棧幀會(huì)銷毀,由于變量x被f3()引用了,x不會(huì)被銷毀。
PS:說(shuō)明下棧幀的概念,一個(gè)程序會(huì)占用一段虛擬內(nèi)存空間。除了某些靜態(tài)字段(比如常量/代碼段)外分為2個(gè)區(qū)域,棧和堆,棧存放靜態(tài)分配的內(nèi)存,自動(dòng)增長(zhǎng)和回收。比如聲明一個(gè)變量,程序棧的部分就會(huì)增長(zhǎng)來(lái)存放這個(gè)變量,變量銷毀后,棧的這部分被自動(dòng)回收。而像C++中動(dòng)態(tài)malloc/new分配一段內(nèi)存,則會(huì)增長(zhǎng)堆的部分,這部分得程序手動(dòng)回收。在python等相對(duì)更高級(jí)的語(yǔ)言中會(huì)用虛擬機(jī)/解釋器的垃圾回收功能來(lái)回收。那函數(shù)的棧幀呢?函數(shù)和類一樣,都可以看作高級(jí)對(duì)象嘛。

C++的閉包

實(shí)現(xiàn)方式

C++的函數(shù)對(duì)象是用類和operator ()來(lái)實(shí)現(xiàn)的,讓對(duì)象的行為表現(xiàn)得像函數(shù)一樣。

struct F2 {
    int x = 1;
    int y = 2;
    int z = 3;
    struct F3 {
        int x;
        int y = 3;
        F3(int x_) : x(x_) {}
        void operator()() const {
            printf("%d %d\n", x, y);
        }
    };
    F3 operator()() const {
        return F3(x);
    }
};

調(diào)用方式如下

    auto f = F2()();
    f();

C++11的lambda表達(dá)式使上述代碼得到了簡(jiǎn)化

#include <functional>

struct F2 {
    int x = 1;
    int y = 2;
    int z = 3;
    std::function<void (void)> operator()() const {
        auto& x = this->x;
        return [x]() { int y = 3; printf("%d %d\n", x, y); };
    }
};

這里注意一點(diǎn),auto& x = this->x這句是必要的,因?yàn)樵L問(wèn)類成員變量的方式實(shí)際上是this->x,只能捕獲this而不能捕獲類的內(nèi)部成員,除非像這樣較為蹩腳的做法,定義一個(gè)局部引用指向類成員變量,再捕獲引用對(duì)象的拷貝。
在C++14中可以用[x = x]的捕獲方式,gcc編譯選項(xiàng)為-std=c++14
上述內(nèi)容參考這篇文章lambda表達(dá)式

C++的問(wèn)題

其實(shí)從上述代碼就可以看出來(lái)了,這里是捕獲一份拷貝,而非引用。如果成員變量x是個(gè)體積較為龐大的對(duì)象呢?比如std::vector<int>std::array<int, 1000>之類的?拷貝的開(kāi)銷還是很大的,那么拷貝引用如何?

    std::function<void (void)> operator()() const {
        auto& x = this->x;
        return [&x]() { int y = 3; printf("%d %d\n", x, y); };
    }

調(diào)用方式如下

    auto f = F2()();
    f();

在這里的測(cè)試代碼是沒(méi)問(wèn)題的,大概是編譯器優(yōu)化,臨時(shí)對(duì)象還存活?但如果像下面這樣

std::function<void (void)> foo()
{
    F2 f2;
    return f2();
}

int main() {
    auto f = foo();
    f();
    return 0;
}

打印結(jié)果如下

xyz@ubuntu:~/python-learning/function/closure$ g++ test.cc -std=c++11
xyz@ubuntu:~/python-learning/function/closure$ ./a.out 
-1558910688 3

因?yàn)閘ambda表達(dá)式捕獲的引用對(duì)象已經(jīng)隨著類的對(duì)象銷毀而銷毀了,這和C的懸掛指針如出一撤,也是C++er常犯的問(wèn)題。比如下面這樣的代碼

const char* foo()
{
    char buf[100];
    // ...
    return buf;
}

上述代碼的問(wèn)題是,函數(shù)foo()調(diào)用完畢時(shí),棧幀銷毀,返回值(char[100]強(qiáng)制轉(zhuǎn)換成的const char*類型)指向的數(shù)組buf是foo的棧幀的一部分,被重新回收,因此返回值指向的是被回收的對(duì)象。

C++做不到像python那樣對(duì)象銷毀時(shí)只銷毀其中一部分變量,而那些被捕獲的變量則保留。
大致而言有兩種解決方式

  1. 把被捕獲的變量聲明為static類型,這樣便不會(huì)隨著對(duì)象銷毀而銷毀。缺點(diǎn)是static變量和全局變量一樣,伴隨著程序整個(gè)生存周期,會(huì)占用不必要的內(nèi)存。而且在多線程環(huán)境下,多個(gè)函數(shù)對(duì)象訪問(wèn)static變量會(huì)產(chǎn)生race condition。
  2. 使用std::shared_ptr<T>來(lái)存儲(chǔ)size較大的T對(duì)象,相當(dāng)于模擬GC的引用計(jì)數(shù)功能。不過(guò)shared_ptr仍然存在線程安全問(wèn)題,依舊得上鎖訪問(wèn)T對(duì)象。
#include <stdio.h>
#include <vector>
#include <functional>
#include <memory>
#include <numeric>

struct Func {
    std::shared_ptr<std::vector<int>> pVec;

    explicit Func(size_t num) {
        pVec = std::make_shared<std::vector<int>>(num);
        std::iota(pVec->begin(), pVec->end(), 0);
    }

    std::function<void (void)> operator()() {
        auto pVec = this->pVec;
        return [pVec]() { for (int i : *pVec) printf("%d ", i); };
    }
};

std::function<void (void)> foo(size_t num) {
    Func func{ num };
    return func();
}

int main() {
    auto f = foo(25);
    f();
    return 0;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 第2章 基本語(yǔ)法 2.1 概述 基本句法和變量 語(yǔ)句 JavaScript程序的執(zhí)行單位為行(line),也就是一...
    悟名先生閱讀 4,506評(píng)論 0 13
  • Lua 5.1 參考手冊(cè) by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 14,235評(píng)論 0 38
  • 一、 前五百年,是她的第一世。 那時(shí)候,她是無(wú)凈佛山下萬(wàn)多曇花中的一朵。受了無(wú)上的佛光普照,通了慧...
    南山之木閱讀 495評(píng)論 1 4
  • 昨晚肚子痛,痛的覺(jué)都睡不著。而他卻睡的像個(gè)豬一樣。沒(méi)生孩子以前從未有過(guò)痛經(jīng),生完孩子后就有痛經(jīng)而且痛一個(gè)多星期。這...
    淡陌染閱讀 477評(píng)論 3 1
  • ////////////////////////////////////高斯濾波/////////////////...
    LCCCC_0523閱讀 5,180評(píng)論 0 0

友情鏈接更多精彩內(nèi)容