1. 程式人生 > 其它 >C ++ lambda表示式的生命週期是多少?

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 =
 [x](int y) { return x + y; };
    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
 operator ()(int y) { return x + y; }

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<>建構函式引數中。

ref:C ++ lambda表示式的生命週期是多少? | 碼農家園 (codenong.com)