【问题标题】:Lambda with dynamic storage duration具有动态存储持续时间的 Lambda
【发布时间】:2016-06-20 14:23:49
【问题描述】:

根据 cppreference.com,C++11 lambda 字面量语法仅在 direct initialization 中使用是合法的。似乎没有办法直接将 lambda 语法与 new 运算符一起使用。

我需要在堆中存储一个 lambda 函数,以便以后可以从不同的线程调用它。制作 lambda 的副本很容易,但是有没有一种简单的方法可以直接在堆中分配 lambda(动态存储持续时间),而无需先在堆栈上分配它(自动存储持续时间)并制作副本?

这是一个简单的例子:

#include <cstdio>
#include <cassert>

struct MyObj {
    int value;
    int copies;
    int moves;

    MyObj(int v): value(v), copies(0), moves(0) {
        printf("Created object with value %d.\n", value);
    }

    MyObj(const MyObj &other): value(other.value),
    copies(other.copies+1), moves(other.moves) { }

    MyObj(const MyObj &&other): value(other.value),
    copies(other.copies), moves(other.moves+1) { }
};

int main (int argc, char **argv) {
    MyObj o { 5 };
    // Create lambda on stack (automatic storage duration)
    auto f = [o] {
        printf("Object value is %d\n", o.value);
        printf("%d copies, %d moves...\n", o.copies, o.moves);
    };
    // Copy lambda to heap (dynamic storage duration)
    decltype(f) *g = new decltype(f)(f);
    // Call the copy
    (*g)();
    return 0;
}

上述程序制作了o 的2 个副本(一个在捕获中,另一个在将lambda 复制到堆中时)。理想情况下,只有一个副本或移动,当堆分配的 lambda 捕获 o 的副本时会发生这种情况。

【问题讨论】:

  • 为什么不直接创建一个命名仿函数并使用它呢?
  • 这个例子被简化了。在所有地方声明函子而不是 lambdas 当然是可能的(你总是可以这样做),但这会使代码更加冗长/繁琐。
  • 我不知道用 lambda 这样做是否合法(我认为可以,但不能确定)但是你不能将变量设为静态并传递它吗引用?比如:coliru.stacked-crooked.com/a/c1a9a420bcb69b8c
  • @NathanOliver - 抱歉回复晚了。这仅在 lambda 没有闭包(即没有捕获)时才有效,因为需要为 lambda 的每个动态实例创建一个唯一的闭包。

标签: c++ c++11 lambda


【解决方案1】:

在 C++11 中,lambda 表达式将总是产生某种形式的自动对象,无论是堆栈变量还是未命名的临时对象。你无法改变这一点。

在 C++17 中,保证省略使我们能够做到这一点:

new auto(<lambda>)

这使用new 分配的内存来存储该表达式的结果。这里不会创建临时 lambda 对象,也不会调用 lambda 的任何复制/移动构造函数。最重要的是,该语言不要求 lambda 类型具有可以调用的复制/移动构造函数。

需要保证省略以确保这一点。如果没有该保证,那么您将依靠编译器对其进行优化。标准允许这种情况下省略副本。是的,任何值得使用的编译器都可能会忽略这些副本。

通过保证省略,您可以捕获固定类型,并且这仍然可以在不复制任何内容的情况下工作。在 C++17 之前,您的 lambda 仍然需要有一个复制或移动构造函数,即使对它的调用被省略了。

【讨论】:

  • 这很有帮助。我不知道 C++17 (可能)会保证复制省略。谢谢!
  • @DaoWen:在接下来的一周里,我们会看看它是否会进入 C++17。
  • @NicolBolas 既然保证复制 elison 是 c++ 17 的一部分,请注意更新此内容?我想用它作为this的欺骗目标@
  • @NathanOliver:好的。
【解决方案2】:

auto 关键字在 new 表达式中是合法的,这允许您这样做:

    // Create lambda directly in heap (dynamic storage duration)
    auto g = new auto([o] {
        printf("Object value is %d\n", o.value);
        printf("%d copies, %d moves...\n", o.copies, o.moves);
    });

这是整个(更新的)示例:

#include <cstdio>
#include <cassert>

struct MyObj {
    int value;
    int copies;
    int moves;

    MyObj(int v): value(v), copies(0), moves(0) {
        printf("Created object with value %d.\n", value);
    }

    MyObj(const MyObj &other): value(other.value),
    copies(other.copies+1), moves(other.moves) { }

    MyObj(const MyObj &&other): value(other.value),
    copies(other.copies), moves(other.moves+1) { }
};

int main (int argc, char **argv) {
    MyObj o { 5 };
    // Create lambda directly in heap (dynamic storage duration)
    auto g = new auto([o] {
        printf("Object value is %d\n", o.value);
        printf("%d copies, %d moves...\n", o.copies, o.moves);
    });
    // Call heap lambda
    (*g)();
    return 0;
}

以上内容仅根据需要制作了o 的一份副本——至少在我的平台上(Apple LLVM 版本 7.0.0 (clang-700.1.76))。

【讨论】:

  • 这是优化,不是保证。
  • 我猜这是某种 RVO?如果它可以在 OS X 和 Linux 上与半新的 Clang 和 GCC 一起使用,那么这可能就足够了——但我仍然对其他解决方案持开放态度,尤其是如果有更便携的解决方案!
  • 是的,这就是复制省略(复制省略是所有形式的“RVO”、参数复制省略等的总称)。看起来很奇怪,但是堆分配与省略的堆栈分配没有什么不同。
  • @DaoWen 只是RVO,NRVO不保证执行。编译器可以选择不使用它。
  • 不错,只有一个副本,没有移动(g++ 4.8.4)
【解决方案3】:

您可以考虑使用 std::make_unique 之类的东西:

template <typename Lambda>
std::unique_ptr<Lambda> make_unique_lambda(Lambda&& lambda)
{
    return std::unique_ptr<Lambda>(
        new Lambda(std::forward<Lambda>(lambda))
    );
}

auto unique_lambda = make_unique_lambda([] () {
    // ...
});

【讨论】:

    【解决方案4】:

    可以跨线程移动的多态函数容器是 std::function,它可以由 lambda 直接初始化。

    int main (int argc, char **argv) {
        std::function<void()> g = [o = MyObj{5}]{
            printf("Object value is %d\n", o.value);
            printf("%d copies, %d moves...\n", o.copies, o.moves);
        };
    
        // Call the copy
        g();
    
        // now *move* it to another thread and call it there
    
        std::thread t([my_g = std::move(g)] { my_g(); });
        t.join();
        return 0;
    }
    

    预期:

    Created object with value 5.
    Object value is 5
    0 copies, 1 moves...
    Object value is 5
    0 copies, 1 moves...
    

    【讨论】:

    • 这给出了结果2 copies, 1 moves,而我原来的例子只有2 copies, 0 moves。即,这比我在我的问题中包含的代码做了更多的复制+移动。我的编译器还警告 my_g = std::move(g) 是 C++14,这不一定是问题,但仍然需要注意(我们宁愿保持依赖关系尽可能简单)。
    • 如果您想最小化副本,我们可以做到。会更新。
    • 先把它移到g有什么好处,f不能在声明auto的时候才移动?
    • @RichardHodges:请阅读这个问题:“理想情况下,只有一个副本或移动”。简单地将 2 个副本变成 2 个动作并不是解决方案。
    • @RichardHodges:这要求捕获的值可以表示为在 lambda 捕获中构建的临时值。正如 OP 在评论中解释的那样,“示例已简化”;实际代码可能更复杂。您过于专注于简化示例的工作,而对避免额外 lambda 副本的一般问题不够关注。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-25
    • 2016-07-05
    • 2015-04-28
    • 2019-08-09
    • 1970-01-01
    • 2021-04-29
    相关资源
    最近更新 更多