【问题标题】:C++11 lambda returning lambdaC++11 lambda 返回 lambda
【发布时间】:2012-09-20 07:30:56
【问题描述】:

这段代码对于 JS 开发者来说并不陌生

function get_counter()
{
    return (
        function() {
            var c = 0;
            return function() { return ++c; };
        })();
}

它基本上创建了一个创建不同枚举数的。所以我想知道是否可以在 C++11 中使用新的 lambda 语义来完成同样的事情?我最终写了这段 C++ 不幸的是不能编译!

int main()
{
    int c;
    auto a = [](){
        int c = 0;
        return [&](){
            cout << c++;
        };
    };
    return 0;
}

所以我想知道是否有一种解决方法来编译它,以及编译器是否可以让这段代码正确运行?我的意思是它必须创建单独的枚举器,但它也应该收集垃圾(未使用的 c 变量)。

顺便说一句,我使用的是 VS2012 编译器,它会产生这个错误:

Error   2   error C2440: 'return' : cannot convert from 'main::<lambda_10d109c73135f5c106ecbfa8ff6f4b6b>::()::<lambda_019decbc8d6cd29488ffec96883efe2a>' to 'void (__cdecl *)(void)'    c:\users\ali\documents\visual studio 2012\projects\test\test\main.cpp   25  1   Test

【问题讨论】:

  • @ecatmur vs2012 在这里我已经更新并添加了我收到的错误消息,除了你知道那段代码是否安全吗?我的意思是它是在浪费内存还是在某处实现了一些隐藏的垃圾收集器?
  • 没有垃圾收集器,也不浪费内存。每次调用a,都会返回一个新的 lambda 对象。您必须将 a 的返回值分配给某物——当该物被销毁时,lambda 用于捕获变量的任何资源都将随之销毁。
  • 在 C++11 中,您需要一个 return expr; 形式的主体来进行返回类型推导,而您没有。如果没有后 C++11 的扩展返回类型推导规则,此代码在任何情况下都不应该编译。如果 VS2012 有这些,我会感到惊讶。
  • @Xeo 事实上你是第一个(除了我;))意识到这一点。到目前为止,所有其他答案都集中在 by-ref 捕获上(这当然也是一个错误,但不是编译器错误)。
  • 所以您的代码中有 2 个错误。一种是返回一个带有(可能)悬空引用的对象,另一种是不指定适当的返回类型(并导致编译器错误)。

标签: c++ visual-c++ lambda c++11 visual-studio-2012


【解决方案1】:

一旦变量不再存在,通过引用捕获的 lambda 不能再使用捕获的变量,这是 C++ 的一个自然限制。所以即使你让它编译,你也不能从它出现的函数中返回这个 lambda(这也恰好是一个 lambda,但这无关紧要),因为自动变量 c 在返回时被销毁。

我认为你需要的代码是:

return [=]() mutable {
    cout << c++;
};

我没有测试过,也不知道有哪些编译器版本支持它,但那是按值捕获,mutable 表示捕获的值可以被 lambda 修改。

因此,每次调用 a 时,您都会得到一个不同的计数器,其计数从 0 开始。每次调用该计数器时,它都会增加自己的 c 副本。据我了解 Javascript(不远),这就是你想要的。

【讨论】:

  • 遗憾的是,使用符合标准的 C++11 编译器,这仍然行不通,因为不会推断返回类型。 ://
  • @Xeo:真的吗?不是void,因为没有尾随返回类型并且lambda 的主体不是return 语句?无论如何,如果有帮助,请将其设为return [=]() mutable -&gt; void。我认为这个cout &lt;&lt; c++; 无论如何只是为了调试,为了匹配Javascript,它将是return ++c;,这将允许返回类型扣除(并且从1而不是0开始计数)。
  • @SteveJessop 不,外部 lambda(分配给 a)。看我的回答。
  • +1 用于解决悬空参考问题。然而,仅有效是什么意思? lambda 将只包含一个悬空引用,我们有 UB。
  • 5.1.2:22 [ 注意:如果实体通过引用隐式或显式捕获,则在实体生命周期结束后调用相应 lambda 表达式的函数调用运算符是可能导致未定义的行为。 —尾注]我没有看到当时存在的 lambda 有任何问题。
【解决方案2】:

您的代码有一个错误,它包含一个悬空引用; c 引用将引用外部 lambda 中的局部变量,当外部 lambda 返回时,该变量将被销毁。

您应该使用 mutable 按值 lambda 捕获来编写它:

auto a = []() {
    int c = 0;
    return [=]() mutable {
        cout << c++;
    };
};

这依赖于后标准扩展,以允许在返回类型推导 lambda 中使用多个语句; Is there a reason on not allowing lambdas to deduce the return type if it contains more than one statement? 最简单的修复方法是提供一个参数,使 lambda 只包含一条语句:

auto a = [](int c) {
    return [=]() mutable {
        cout << c++;
    };
};

不幸的是,lambdas 中不允许使用默认参数,因此您必须将其称为 a(0)。或者,以可读性为代价,您可以使用嵌套的 lambda 调用:

auto a = []() {
    return ([](int c) {
        return [=]() mutable {
            cout << c++;
        };
    })(0);
};

其工作方式是,当a 执行内部 lambda 时,会将所有引用的变量复制到其闭包类型的实例中,如下所示:

struct inner_lambda {
    int c;
    void operator()() { cout << c++; }
};

闭包类型的实例随后由外部 lambda 返回,并可被调用,并在调用时修改其 c 的副本。

总体而言,您的(固定)代码被翻译为:

struct outer_lambda {
    // no closure
    struct inner_lambda {
        int c;    // by-value capture
        // non-const because "mutable"
        void operator()() { cout << c++; }
    }
    // const because non-"mutable"
    inner_lambda operator()(int c) const {
        return inner_lambda{c};
    }
};

如果您留下 c 作为按引用捕获,则为:

struct outer_lambda {
    // no closure
    struct inner_lambda {
        int &c;    // by-reference capture
        void operator()() const { cout << c++; } // const, but can modify c
    }
    inner_lambda operator()(int c) const {
        return inner_lambda{c};
    }
};

这里的inner_lambda::c是对局部参数变量c的悬空引用。

【讨论】:

  • +1 来自我,引入该参数的风险很小,当某人实际上没有权利使用该参数时可能会指定它,因为该功能不是设计的。但实际上这是对这个反生成器功能的一个很好的扩展,所以将它添加到设计中并继续前进:-)
  • @SteveJessop 结果表明 lambdas (5.1.2:5) 中不允许使用默认参数。有点烦人,那个。
  • 是的,可能是因为 lambdas 旨在被std::function 捕获,并且一旦您删除类型,您就失去了默认参数的值。遗憾的是,对于 不是 类型擦除的 lambda,无论如何你都不能拥有它们。我猜它不被认为是一个主要用例,但正如您所展示的那样,从函数返回 lambda 提供了一些用例,可以方便地返回可以使用可选参数调用的东西。
  • +1,您的“嵌套 lambda 调用”方法正是我会做的。
【解决方案3】:

首先你应该知道的是,即使你得到了要编译的语法,语义也是不同的。在按引用捕获的 C++ lambda 中,仅捕获一个普通引用,这不会延长该引用绑定的对象的生命周期。也就是说,c 的生命周期与封闭 lambda 的生命周期绑定:

int main()
{
    int c;
    auto a = [](){
        int c = 0;
        return [&](){
            return ++c;
        };
    }();                     // Note: added () as in the JS case
    std::cout << a() << a();
    return 0;
}

在添加缺少的() 以便评估外部 lambda 之后,您的问题是返回的 lambda 中通过引用保存的 c 在评估完整表达式后不再有效。

话虽如此,以额外的动态分配为代价(相当于 JS 的情况)使这项工作并不复杂:

int main()
{
    int c;
    auto a = [](){
        std::shared_ptr<int> c = std::make_shared<int>(0);
        return [=](){
            return ++(*c);
        };
    }();                     // Note: added () as in the JS case
    std::cout << a() << a();
    return 0;
}

应该按预期编译和工作。每当内部 lambda 被释放(a 超出范围)时,计数器将从内存中释放。

【讨论】:

    【解决方案4】:

    我认为问题在于编译器无法推断出外部 lambda(分配给 a)的返回类型,因为它包含的不仅仅是简单的一行返回。但不幸的是,也没有办法明确说明内部 lambda 的类型。所以你必须返回一个std::function,这会带来一些额外的开销:

    int main()
    {
        int c;
        auto a = []() -> std::function<void()> {
            int c = 0;
            return [=]() mutable {
                std::cout << c++;
            };
        };
        return 0;
    }
    

    当然,您必须按价值捕获,就像 Steve 在他的回答中已经解释的那样。

    编辑: 至于为什么确切的错误是它无法将返回的内部 lambda 转换为 void(*)()(指向 void() 函数的指针),我只有一些猜测,因为我没有对他们的 lambda 实现有很多了解,我不确定它是否稳定或符合标准。

    但我认为 VC 至少尝试推断出内部 lambda 的返回类型并意识到它返回一个可调用对象。但后来它以某种方式错误地假设这个内部 lambda 没有捕获(或者他们无法确定内部 lambda 的类型),所以他们只是让外部 lambda 返回一个简单的函数指针,如果内部 lambda 不会,这确实可以工作捕捉任何东西。

    编辑: 就像 ecatmur 在他的评论中所说,在创建实际的 get_counter 函数(而不是 lambda)时甚至需要返回 std::function,因为普通函数没有任何自动返回类型推导。

    【讨论】:

    • 这里有一个错误:默认情况下按值捕获不允许修改被捕获的对象(隐式生成的operator()const),也就是说内部的c++ lambda 将无法编译。请注意,外部 lambda 不必是可变的。
    • @DavidRodríguez-dribeas 您指的是旧版本(外部 lambda 不是 mutable)还是仍然有错误?啊,忘了我说的话。已找到并更正,谢谢。
    • 我可能错了,我对 lambda 没有太多经验,但 inner lambda 按值捕获,然后尝试修改该值。 IIRC 要求内部 lambda 是可变的。
    • std::function 是一个很好的建议,如果a 是一个实际的函数,它是必要的。
    【解决方案5】:

    这适用于 g++ 4.7

    #include <iostream>
    #include <functional>                                                                           
    
    std::function<int()> make_counter() {
        return []()->std::function<int()> {
            int c=0;
            return [=]() mutable ->int {
                return  c++ ;
            };  
        }();
    }   
    
    
    int main(int argc, char * argv[]) {
        int i = 1;
        auto count1= make_counter();
        auto count2= make_counter();
    
        std::cout << "count1=" << count1() << std::endl;
        std::cout << "count1=" << count1() << std::endl;
        std::cout << "count2=" << count2() << std::endl;
        std::cout << "count1=" << count1() << std::endl;
        std::cout << "count2=" << count2() << std::endl;
        return 0;
    }
    

    Valgrind 完全没有抱怨这一点。每次我调用 make_counter 时,valgrind 都会报告一个额外的分配和空闲,所以我假设 lambda 元编程代码正在为变量 c 插入内存​​分配代码(我想我可以检查调试器)。我想知道这是否符合 Cxx11 或只是 g++ 特定的。 Clang 3.0 不会编译它,因为它没有 std::function(也许我可以尝试使用 boost 函数)。

    【讨论】:

      【解决方案6】:

      我知道这已经晚了,但在 C++14 及更高版本中,您现在可以初始化 lambda 捕获,从而获得更简单的代码:

      auto a = []() {
          return [c=0]() mutable {
              cout << c++;
          };
      };
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-12-28
        • 2012-11-10
        • 2014-03-31
        • 2019-02-09
        • 1970-01-01
        • 2014-02-23
        • 2015-01-08
        • 1970-01-01
        相关资源
        最近更新 更多