【问题标题】:What is the lifetime of a C++ lambda expression?C++ lambda 表达式的生命周期是多少?
【发布时间】:2011-12-17 23:59:34
【问题描述】:

(我已经阅读了What is the lifetime of lambda-derived implicit functors in C++?,它没有回答这个问题。)

我了解 C++ lambda 语法只是用于创建具有调用运算符和某些状态的匿名类的实例的糖,并且我了解该状态的生命周期要求(取决于您是否通过引用的值捕获。)但是 lambda 对象本身的生命周期是多少?在以下示例中,返回的 std::function 实例是否有用?

std::function<int(int)> meta_add(int x) {
    auto add = [x](int y) { return x + y; };
    return add;
}

如果是,它是如何工作的?这对我来说似乎有点太神奇了——我只能想象它通过 std::function 复制我的整个实例来工作,根据我捕获的内容,这可能非常重——过去我主要使用了 std::function 与裸函数指针,复制它们很快。鉴于std::function 的类型擦除,这似乎也有问题。

【问题讨论】:

    标签: c++ lambda c++11


    【解决方案1】:

    生命周期与将 lambda 替换为手动仿函数的情况完全相同:

    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 函数的本地,然后将 [in its entirty, including the value of x] 移动到返回值中,然后本地实例将超出范围并被销毁为普通的。但只要持有它的std::function 对象,从函数返回的对象将保持有效。这显然取决于调用上下文。

    【讨论】:

    • 问题是,我不知道这实际上也适用于命名类,它让我无所适从。
    • 如果你不熟悉 C++11 之前的函数对象是如何工作的,你应该看看那些,因为 lambda 几乎只是函数对象的语法糖。一旦你明白了,很明显 lambda 具有与函数对象相同的值语义,因此它们的生命周期是相同的。
    • 正常(在堆栈上分配)生命周期,但返回值优化?
    • 不会在返回时添加被复制,而不是移动?一个真正的 lambda 会被移动吗?我不知道为什么不能这样,但也许它实际上不是这样工作的??
    【解决方案2】:

    您似乎对 std::function 比对 lambdas 更困惑。

    std::function 使用一种称为类型擦除的技术。快速浏览一下。

    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)

    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 基本上是传递它们的唯一方法(如果我有一个命名类型,我只会返回一个命名类型的实例,这很明显) ,然后我不知道实例去了哪里。
    • 这只是示例代码,缺少一些细节。它会泄漏内存,并且缺少对std::movestd::forward 的调用。还有std::function一般会使用小对象优化来避免T小的时候使用堆。
    • 对不起,我正在尝试了解擦除类型是什么,并且在您的示例中擦除类中您有 m_erased.call(f)。如果 m_erased 是一个成员指针,你怎么能做 m_erased.call(f)?我试图将点更改为箭头,我认为它正在尝试访问 Base 纯虚函数。这是因为这只是一个例子吗?我已经盯着它看了十分钟,我想我快疯了。谢谢
    • @TitoneMaurice:是的,那绝对应该是@​​987654332@。为什么你认为它会调用基础虚函数?请记住,即使派生类省略了virtual 关键字,虚函数也会被重载
    【解决方案3】:

    这是:

    [x](int y) { return x + y; };
    

    相当于:(或者也可以考虑)

    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 不是一回事;这只是一种将通用可调用对象包装在对象中的方法。
    • @NicolBolas:嗯,我通过 std::function 返回是有原因的,因为那是我不明白的步骤。正如丹尼斯所说,这也适用于命名类,我不知道 - 大约在过去的一年里(在我开始使用 std::function 之后但在我开始使用 lambdas 之前)我一直认为它不起作用。跨度>
    • lambda 将被移动到 std::function&lt;&gt; 实例中,而不是被复制,因此拥有一个定义良好的复制构造函数是无关紧要的 - move 构造函数是相关的。
    【解决方案4】:

    在您发布的代码中:

    std::function<int(int)> meta_add(int x) {
        auto add = [x](int y) { return x + y; };
        return add;
    }
    

    函数返回的 std::function&lt;int(int)&gt; 对象实际上保存了一个已移动的 lambda 函数对象实例,该实例已分配给局部变量 add

    当您定义捕获按值或按引用的 C++11 lambda 时,C++ 编译器会自动生成唯一的函数类型,该类型的实例是在调用 lambda 或将其分配给变量时构造的。为了说明,您的 C++ 编译器可能会为 [x](int y) { return x + y; } 定义的 lambda 生成以下类类型:

    class __lambda_373s27a
    {
        int x;
    
    public:
        __lambda_373s27a(int x_)
            : x(x_)
        {
        }
    
        int operator()(int y) const {
            return x + y;
        }
    };
    

    那么,meta_add 函数本质上等价于:

    std::function<int(int)> meta_add(int x) {
        __lambda_373s27a add = __lambda_373s27a(x);
        return add;
    }
    

    编辑:顺便说一句,我不确定你是否知道,但这是 C++11 中函数 currying 的示例。

    【讨论】:

    • 实际上,该函数返回的std::function&lt;int(int)&gt; 对象包含一个已移动 lambda 函数对象的实例——不执行任何副本。
    • ildjarn:meta_add(int) 函数是否需要返回std::move(add) 才能调用函数类型的移动构造函数(在这种情况下为__lambda_373s27a)?
    • 不,return 语句允许隐式地将返回的值视为右值,使其隐式可移动并消除对显式 return std::move(...); 的需要(防止 RVO/NRVO,实际上使 @ 987654335@ 反模式)。因此,因为 addreturn 语句中被视为右值,因此 lambda 被移动到 std::function&lt;&gt; 构造函数参数中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-16
    • 2016-05-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多