C ++ lambda表示式的生命週期是多少?
What is the lifetime of a C++ lambda expression?
(我已經閱讀了C ++中lambda派生的隱式仿函式的生命週期是什麼?已經沒有回答這個問題了。)
我理解C ++ lambda語法只是用於建立具有呼叫操作符和某個狀態的匿名類的例項的糖,並且我理解該狀態的生存期要求(由您是否通過引用的值捕獲來決定。)但是什麼是 lambda物件本身的生命週期? 在以下示例中,返回的std::function例項是否有用?
1 2 3 4 |
std::function<int(int)> meta_add(int x) { auto add = return add; } |
如果是,它是如何工作的? 這對我來說似乎有點太神奇了 - 我只能想象它是通過std::function複製我的整個例項來工作的,這可能是非常沉重的,這取決於我捕獲的內容 - 過去我主要使用std::function使用裸功能 指標,複製它們很快。 鑑於std::function的型別擦除,它似乎也有問題。
如果用手動編織器代替lambda,它的生命週期正是如此:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct lambda { lambda(int x) : x(x) { } int private: int x; }; std::function<int(int)> meta_add(int x) { lambda add(x); return add; } |
該物件將在meta_add函式的本地建立,然後[在其entirty中,包括x的值]移動到返回值中,然後本地例項將超出範圍並正常銷燬。但是隻要儲存它的std::function物件,該函式返回的物件將保持有效。這多長時間顯然取決於呼叫上下文。
相關討論- 問題是,我不知道這實際上也適用於命名類,並且它讓我感到困惑。
- 如果您不熟悉C ++ 11之前函式物件的工作方式,那麼您應該看一下這些函式物件,因為lambdas幾乎不是函式物件的語法糖。一旦你理解了lambdas與函式物件具有相同的值語義,那麼它們的生命週期是相同的。
- 正常(在堆疊上分配)生命週期,但返回值優化?
- 不會在返回時新增複製,而不是移動?一個真正的lambda會被移動嗎?我不知道為什麼它不能成為任何理由,但也許那不是它實際上是如何工作的?
看起來你對std::function比對lambdas更困惑。
std::function使用稱為型別擦除的技術。這是一個快速飛過。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class Base { virtual ~Base() {} virtual int call( float ) =0; }; template< typename T> class Eraser : public Base { public: Eraser( T t ) : m_t(t) { } virtual int call( float f ) override { return m_t(f); } private: T m_t; }; class Erased { public: template<typename T> Erased( T t ) : m_erased( new Eraser< T >(t) ) { } int do_call( float f ) { return m_erased->call( f ); } private: Base* m_erased; }; |
你為什麼要刪除這個型別?不是我們想要的型別int (*)(float)嗎?
型別擦除允許的是Erased現在可以儲存任何可呼叫的值,如int(float)。
1 2 3 4 5 6 7 8 9 10 11 |
int boring( float f); short interesting( double d ); struct Powerful { int operator() ( float ); }; Erased e_boring( &boring ); Erased e_interesting( &interesting ); Erased e_powerful( Powerful() ); Erased e_useful( []( float f ) { return 42; } ); |
- 回想起來,我對std :: function感到困惑,因為我不知道它保留了很多東西的所有權。我假設將一個例項"包裝"到std :: function後該例項左邊的範圍無效。 lambdas將混淆帶到頭部的原因是因為std :: function基本上是傳遞它們的唯一方法(如果我有一個命名型別,Id只返回一個命名型別的例項,而且相當明顯),然後我不知道例項去了哪裡。
- 這只是示例程式碼,缺少一些細節。 它洩漏了記憶體,並且缺少對std::move,std::forward的呼叫。 此外,std::function通常使用小物件優化來避免在T較小時使用堆。
- 對不起,我試著去學習什麼型別的擦除,並且在你的例子中,Erased你有m_erased.call(f)。 如果m_erased是成員指標,你怎麼能做m_erased.call(f)? 我試圖將點更改為箭頭,我認為它試圖訪問Base純虛擬函式。 這是因為它只是一個例子嗎? 我一直盯著它看了十分鐘,我覺得我很生氣。 謝謝
- @TitoneMaurice:是的,那絕對應該是->。 為什麼你認為它會呼叫基本虛擬函式? 請記住,即使派生類省略了virtual關鍵字,虛擬函式也會被過載
這是:
1 | [x](int y) { return x + y; }; |
相當於:(或者也可以考慮)
1 2 3 4 5 6 7 |
struct MyLambda { MyLambda(int x): x(x) {} int operator()(int y) const { return x + y; } private: int x; }; |
所以你的物件正在返回一個看起來就像那樣的物件。其中有一個定義良好的複製建構函式。因此,它可以從函式中正確複製似乎非常合理。
相關討論- 要將它複製出函式,需要std :: function在例項化std :: function之後知道它的型別。它怎麼能這樣做?想到的唯一技巧是指向帶有虛擬函式的模板化類的例項,該函式知道lambda的確切型別。這看起來很糟糕,我甚至不知道它是否真的有效。
- @Joe:你基本上描述了磨機型別擦除的執行,這就是它的工作原理。
- @JoeWreschnig:你沒有問過std::function是如何工作的;你問過lambdas是如何工作的。 std::function不是一回事;它只是一種在物件中包裝泛型callable的方法。
- @NicolBolas:嗯,我通過std :: function返回了一個原因,因為這是我不理解的步驟。正如丹尼斯所說,這也適用於我不知道的命名類 - 對於過去的一年(在我開始使用std :: function之後但在我開始使用lambdas之前),我總是認為它不會起作用。
- lambda將被移動到std::function<>例項中,而不是被複制,因此具有明確定義的複製建構函式是無關緊要的 - 移動建構函式是什麼相關的。
- 嗯,如果是這樣,那麼我不能做(新MyLamba(x))(y)?我不認為這是可能的。
- 另外,移動而不是複製呢?看起來如果lambda副本捕獲許多值,移動和複製之間的差異將是顯著的。
- @allyourcode:評論一。你可以做(new MyLambda(x))->operator()(y)。你通常做的是MyLambda(x)(y)。 new的問題在於您建立的指標不是物件。指標沒有方法,因此您需要將指標取消引用到物件型別。
- @allyourcode:在大多數情況下,複製elidtion(sp)會使副本無效。但要真正回答你需要更加具體地考慮你正在考慮的情況,因為它可能是一個問題。
- @LokiAstari啊是的。不知道我為什麼用新的。但這不是我的觀點:建構函式似乎不存在。為什麼我嘗試使用它,我的編譯器告訴我只有兩個建構函式,並且它們都沒有使用x。
在您釋出的程式碼中:
1 2 3 4 |
std::function<int(int)> meta_add(int x) { auto add = [x](int y) { return x + y; }; return add; } |
函式返回的std::function<int(int)></int(int)>物件實際上包含已分配給區域性變數add的lambda函式物件的移動例項。
當您定義捕獲按值或按引用的C ++ 11 lambda時,C ++編譯器會自動生成一個唯一的函式型別,其例項是在呼叫lambda或賦值給變數時構造的。為了說明,您的C ++編譯器可能會為[x](int y) { return x + y; }定義的lambda生成以下類型別:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class __lambda_373s27a { int x; public: __lambda_373s27a(int x_) : x(x_) { } int operator()(int y) const { return x + y; } }; |
然後,meta_add函式基本上等同於:
1 2 3 4 |
std::function<int(int)> meta_add(int x) { __lambda_373s27a add = __lambda_373s27a(x); return add; } |
編輯:順便說一句,我不知道你是否知道這一點,但這是C ++ 11中函式currying的一個例子。
相關討論- 實際上,函式返回的std::function<int(int)></int(int)>物件包含一個移動的lambda函式物件例項 - 不執行任何副本。
- ildjarn:meta_add(int)函式是否需要返回std::move(add)才能呼叫函式型別的移動建構函式(在本例中為__lambda_373s27a)?
- 不,return語句允許隱式地將返回值視為右值,使其可以隱式移動並且不需要顯式return std::move(...);(這可以防止RVO / NRVO,實際上使return std::move(...);成為反模式)。因為add在return語句中被視為rvalue,因此lambda被移動到std::function<>建構函式引數中。