簡體   English   中英

std::function 內部存儲器組織和副本; 傳遞引用 vs 值

[英]std::function internal memory organization and copies; passing reference vs value

std::function被復制時,它引用的代碼指令是否也被復制?

std::function是通過某種形式的可調用對象初始化的,它以某種方式指向可執行代碼(就像函數指針通常所做的那樣)。 現在,當一個函數對象被復制時,這個可執行代碼運行時是復制的還是內部引用的? 重新表述這個問題:如果復制了一個std::function實例,那么內存中是否有多個相同編譯代碼指令的副本? std::function是實際存儲函數代碼的對象還是更像是函數指針的抽象?

前者看起來很浪費,我不懷疑,但到目前為止我在這個主題上發現的一切要么太模糊,要么太具體,我無法肯定地說。 例如

當目標是函數指針或 std::reference_wrapper 時,保證小對象優化,即這些目標總是直接存儲在 std::function 對象內,不會發生動態分配。 其他大對象可以在動態分配的存儲中構造並由 std::function 對象通過指針訪問。 - cppreference

給出了一些關於它是如何完成的提示,但似乎仍然太模糊,並且可能與這個問題完全無關,因為std::function內部有進一步的抽象。

對於上下文:我正在嘗試重構一些糟糕的 C 語言代碼,這些代碼將輸入事件(擊鍵、鼠標輸入等)映射到特定行為,該行為在目標數據結構上執行,該數據結構可以被程序解釋為更多具有除擊鍵之外的語義上下文的特定輸入(又名鍵綁定)。 人們可以懷疑行為的要求變化很大。
這以前是通過定義和數字列表來實現的,這些列表指定了 input-event-ids,以及由 switch-case 選擇的硬編碼行為。 我們很快接近了這種最初的做法變得笨拙的邊界。
為了從定義的列表中跳出可擴展的、聲明性的、面向對象的和靈活的設計,我考慮了高階函數。
特別是由於某些行為非常簡單且需要重復使用(例如切換輸出數據結構中的一個值),而其他行為在附加多個條件的情況下更加復雜,我想靜態聲明一些行為,但仍然會在某些情況下,喜歡只分配一些特殊的 lambda。 由於我需要存儲每個輸入(鍵、鼠標按鈕、鼠標軸等)的行為,並且可能一次針對不同的鍵綁定集實例化一種特定行為類型的多個副本,我想知道是否應該引用這種行為,而不是按值存儲。 在前一種情況下,新的 lambda 需要由行為結構擁有,但靜態聲明的行為不需要,這在實際中會導致一些shared_ptr惡作劇。 在后一種情況下,根據價值,這不是問題,但我不希望例如切換行為的多個副本導致過多的冗余開銷。

(注意:下面的整個討論有點簡化。AFAIK,沒有錯,但我確實省略了一些細節和邊緣情況以及定義和實現的東西。)

std::function不會復制任何可執行代碼。 可執行代碼始終僅由std::function指向。 std::function被復制時,指針會被復制(這完全沒問題,因為可執行代碼也永遠不會被釋放。)到目前為止,普通的舊函數指針和std::function之間沒有區別。

但這還不是全部。

與函數指針相反, std::function實例可以攜帶“狀態”以及指向可執行代碼的指針,關於std::function必須分配/解除分配和復制/移動數據的整個喧囂就是關於這個額外的狀態,而不是函數指針

假設你有這樣的代碼:

(請注意,雖然我在這里使用了 lambda,但以下解釋同樣適用於 C++ 中的“函子”和“函數對象”和“綁定結果”以及其他形式的可調用事物,除了普通的舊函數指針.)

int x = 42, y = 17;
std::function<int()> f = [x, y] {return x + y;};

這里, f不僅存儲了return x + y;的可執行代碼的指針return x + y; ,但它也必須記住xy的值。 由於您可以通過這種方式“捕獲”的狀態數量不受限制,因此根據定義, std::function必須在構造時從堆分配內存,並在適當的時間釋放、復制和移動它。 同樣,復制的是這個額外的“狀態”,而不是代碼。

讓我們回顧一下:每個std::function至少需要能夠存儲一個指向可執行代碼的指針,以及 0 個或更多字節的額外捕獲狀態。 如果沒有捕獲狀態,則std::function本質上與函數指針相同(盡管在實踐中, std::function通常以多態方式實現並在其中包含其他內容。)

我所知道的std::function一些(大多數)實現采用了一種稱為“小對象優化”的優化。 在這些實現中,除了代碼指針的空間之外, std::function對象在其實例內部還有更多(固定數量的)空間(即作為其類的成員,而不是堆上的其他地方) ) 並且如果捕獲狀態的總字節數適合該區域,則將使用該區域。 這消除了堆分配,這在某些用例中很重要,並且可以平衡使用的額外內存(當沒有或幾乎沒有要捕獲的狀態時)。

我認為有關例外的信息有一些共同之處:

如果 other 的目標是函數指針或 std::reference_wrapper 則不拋出,否則可能拋出 std::bad_alloc 或用於復制或移動存儲的可調用對象的構造函數拋出的任何異常。 Cpp參考

這似乎意味着 std::function 的每個副本也復制包含的可調用對象。 例如,如果您的函數包含帶有向量的 lambda,則會復制該 lambda 和結果向量。 鏈接到它的實際機器代碼保留在可執行文件的只讀部分中,不會被復制。

來自c++20 標准草案的更新:20.14.16.2.1 構造函數和析構函數[func.wrap.func.con]

函數(常量函數& f);

后置條件:!*this if !f; 否則,*this 的目標是 off.target() 的副本。

拋出: iff 的目標不是 reference_wrapper 的特化,也不是函數指針。 否則,可能 throwbad_allocor 存儲的可調用對象的復制構造函數拋出的任何異常。

[注意:實現應該避免為小的可調用對象使用動態分配的內存,例如,f 的目標是一個只保存一個指針或對對象的引用和一個成員函數指針的對象。 — 尾注]

似乎std::function只管理一個可調用的。 如果復制,代碼會發生什么由可調用本身指定。

在函數指針的情況下,只需要復制一個函數指針。

在 lambda 或自定義可調用的情況下,這將由 lambda 或任何自定義可調用類的副本的實現來確定。 后兩者通常可以在代碼引用之外擁有自己的成員。 因此必須由std::function分配一些空間來適應這些情況。 然而,這具有誤導性,因為它看起來似乎是std::function為代碼分配空間。 指令代碼的管理似乎由可調用對象完成,但這是在內部完成的。

在這種情況下,復制時通常使用的可調用對象(如 lambdas)的默認行為對於預期的問題似乎更有趣,但似乎確實將提出的問題超出了std::function的上下文范圍。

因此,我認為這個問題已經解決了,並加深了我對 lamdas 是如何實現的了解,尤其是在它們是如何編譯的和編譯的代碼引用方面。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM