【问题标题】:Is it possible to override C++11 lambda/closure destructor?是否可以覆盖 C++11 lambda/closure 析构函数?
【发布时间】:2015-06-28 18:04:19
【问题描述】:

我正在使用 C++11 的 lambda/closure 功能,尝试编写一个半协程(生成器)函数。

以下代码是我的实验。它们按预期工作(GCC 4.9/Linux)。

看了几篇介绍 C++ lambda/closure 的网上文章,现在我把 lambda 函数理解为函子或函数对象。

我的问题是:我可以将闭包对象的析构函数代码放在哪里?如何覆盖默认析构函数?

我需要这个函数,因为当我调用构造和返回闭包对象(代表协程)的函数时,某些协程可能会请求某些系统资源。但是如果调用者函数决定取消/终止操作,应该有一种机制来通知协程对象采取一些行动。放置代码的理想位置是在析构函数中,但我找不到如何。

有什么建议吗?

#include <iostream>
#include <functional>
#include <time.h>

#define CONCAT01(a, b)                      a##b
#define CONCAT02(a, b)                      CONCAT01(a, b)
#define YIELD()                             \
  do {                                      \
    CONCAT02(ENTRY, __LINE__):              \
    if (EP == && CONCAT02(ENTRY, __LINE__)) \
      EP = 0;                               \
    else                                    \
    {                                       \
      EP = && CONCAT02(ENTRY, __LINE__);    \
      return true;                          \
    }                                       \
  } while (0)

using namespace std;

typedef std::function<bool(void)> coroutine;

coroutine add(int a, int b, int * ret)
{
  timespec start;
  clock_gettime(CLOCK_REALTIME, &start);

  return [=]() mutable -> bool
  {
    timespec now;
    while(1) {
      clock_gettime(CLOCK_REALTIME, &now);
      if (now.tv_sec - start.tv_sec > 1)
        break;

      return true;
    }
    * ret = a + b;
    cout << "add " << a << " " << b << " equals: " << a + b << endl;
    return false;
  };
}

coroutine sum(int a, int b, int c, int * ret)
{
  void* EP = 0;
  coroutine co = 0;
  int tmp = 0;

  return [=] () mutable -> bool
  {
    if (EP) goto *EP;

    co = add(a, b, &tmp);
    while(co())
      YIELD();

    co = add(tmp, c, ret);
    while(co())
      YIELD();

    return false;
  };
}

int main()
{
  int ret;
  coroutine c = sum(1, 2, 4, &ret);
  while (c())
    {
      sleep(1);
      cout << "wakeup" << endl;
    }

  cout << "final: " << ret << endl;

  return 0;
}

【问题讨论】:

  • C++11 但不是 C++14?你知道通过使用std::function 你是在断言你的协程可以被复制?
  • 我不完全理解你所说的“协程可以被复制”是什么意思。上面的代码有效,这正是我们在现实世界代码中使用这种模式的方式。我们不打算将协程对象从一个地方复制到另一个地方。
  • a std::function&lt;bool()&gt; 是一个对象,它被类型擦除直到被() 调用,返回与bool 兼容的类型并且可以被复制。两者都是std::function 所要求的,并且该类型为其消费者提供了保证。如果您不打算同时承诺两者,则可能不应该使用它。
  • @Yakk,我承认可复制的协程听起来很奇怪。但是除了 std::function 之外,还有其他方法可以获取闭包对象的句柄吗?我使用 lambda/closure 部分是因为它可以自动捕获参数变量,我还可以将持久状态放入包含函数中,该函数将自动复制到闭包成员。这样,代码看起来很简单。否则我必须手动编写一个构造函数,这很不方便。
  • @Yakk,实际上我在微控制器上使用了这种模式,以替换 while-loop 状态机模式或消息/事件调度模式,这对于复杂的控制流来说非常尴尬和不自然。所有所谓的协程函数都是一个小型状态机。子功能是子状态机。 “调用堆栈”可以被视为一个线程。到目前为止,我的代码完美无缺。它们看起来都像上面的代码。但我不知道这是一种危险的做法,您能否指出在什么情况下,这种“可复制”功能很容易让事情变得混乱?

标签: c++11 lambda destructor c++14 coroutine


【解决方案1】:

这是一个带有任务和销毁任务的对象:

template<class F, class D>
struct coroutine {
  F f;
  D d;
  template<class...Args>
  std::result_of_t<F&(Args...)>
  operator()(Args&&...args){
    return f(std::forward<Args>(args)...);
  }
  ~coroutine() {
    d();
  }
};

它的工厂函数:

template<class F, class D>
coroutine< std::decay_t<F>, std::decay_t<D> >
make_coroutine( F&& f, D&& d ) {
  return {std::forward<F>(f), std::forward<D>(d)};
}

一些辅助宏:

#define COROUTINE_STATE ED=(void*)0
#define START_COROUTINE if (ED) goto *ED
#define FINISH_COROUTINE return false

auto add(int a, int b, int * ret) {
  timespec start;
  clock_gettime(CLOCK_REALTIME, &start);

  return make_coroutine([=]() mutable
  {
    timespec now;
    while(true) {
      clock_gettime(CLOCK_REALTIME, &now);
      if (now.tv_sec - start.tv_sec > 1)
        break;

      return true;
    }
    *ret = a + b;
    cout << "add " << a << " " << b << " equals: " << a + b << endl;
    return false;
  },
  []{
    cout << "adder destroyed\n";
  });
}
auto sum(int a, int b, int c, int * ret)
{
  std::function<bool()> co;
  int tmp = 0;

  return make_coroutine([=,COROUTINE_STATE] () mutable
    {
      START_COROUTINE;
      co = add(a, b, &tmp);
      while(co())
        YIELD();

      co = add(tmp, c, ret);
      while(co())
        YIELD();

      FINISH_COROUTINE;
    },
    []{
      std::cout << "winter is coming\n";
    }
  );
}

请注意,在此设计中,析构函数无法访问“主体”中的任何状态更改,例如 ED。

您捕获COROUTINE_STATE 以获取ED 指针。然后START_COROUTINE; 进行自动分支,YIELD() 插入跳转点,FINISH_COROUTINE; 结束协程。

这里缺少共享状态是一个严重的问题。主体可以访问的co 与驱逐舰在执行= 时可以访问的co 完全不同。

我们可以通过在您的协程中添加一个新状态来解决这个问题,该状态发生在FINISH_COROUTINE; 或类似的“之后”。注入访问的能力是很棘手的——一种方法是将ED 移出 lambda 捕获本身:

template<class F>
struct coroutine {
  void** ED = nullptr;
  F f;
  template<class...Args>
  std::result_of_t< F&(void**,Args...) >
  operator()(Args&&...args){
    return f(ED, std::forward<Args>(args)...);
  }
  ~coroutine() {
    void* end = 0;
    f(&end);
  }
};
template<class F>
coroutine<std::decay_t<F>>
make_coroutine(F&& f){return {std::forward<F>{f}};}
#define START_COROUTINE [=](void** ED)mutable{\
  if (ED){if (*ED)goto *ED;\
  else goto Cleanup;}
#define END_COROUTINE_BODY return false; Cleanup:
#define END_COROUTINE_CLEANUP }
#define END_COROUTINE END_COROUTINE_BODY; END_COROUTINE_CLEANUP

auto sum(int a, int b, int c, int * ret)
{
  std::function<bool()> co = 0;
  int tmp = 0;

  return
  START_COROUTINE
    co = add(a, b, &tmp);
    while(co())
      YIELD();

    co = add(tmp, c, ret);
    while(co())
      YIELD();
  END_COROUTINE_BODY
    std::cout << "winter is coming\n";
  END_COROUTINE_CLEANUP;
}

或类似的东西。我们明确地传递ED,如果它指向一个nullptr,我们认为它意味着“跳转到清理代码”。这会将清理代码与正文放在同一范围内。

我现在看到你的类型擦除到std::function 可能有用。请注意,这种类型擦除需要内存分配开销(实际上),这是不幸的,并且每次调用都会产生虚函数开销。

遗憾的是,在我想通之前,我重用了你的 coroutine 类型名,所以直接开始使用 std::function&lt;bool()。我很抱歉。

【讨论】:

  • 非常感谢先生。我需要一些时间来弄清楚。我已经多年没有使用 C++ 模板了。我可能需要相当长的时间才能恢复。
  • @matianfu 请注意,我没有编译上面的代码,所以很遗憾它可能包含许多错别字和错误。
猜你喜欢
  • 2014-01-09
  • 1970-01-01
  • 2012-07-01
  • 1970-01-01
  • 2019-06-21
  • 1970-01-01
  • 2013-07-29
  • 1970-01-01
  • 2011-04-06
相关资源
最近更新 更多