1

我使用的几个代码库包括手动调用的类,newdelete采用以下模式:

class Worker {
 public:
  void DoWork(ArgT arg, std::function<void()> done) {
    new Worker(std::move(arg), std::move(done)).Start();
  }

 private:
  Worker(ArgT arg, std::function<void()> done)
      : arg_(std::move(arg)),
        done_(std::move(done)),
        latch_(2) {} // The error-prone Latch interface isn't the point of this question. :)

  void Start() {
    Async1(<args>, [=]() { this->Method1(); });
  }
  void Method1() {
    StartParallel(<args>, [=]() { this->latch_.count_down(); });
    StartParallel(<other_args>, [=]() { this->latch_.count_down(); });
    latch_.then([=]() { this->Finish(); });
  }
  void Finish() {
    done_();
    // Note manual memory management!
    delete this;
  }

  ArgT arg_
  std::function<void()> done_;
  Latch latch_;
};

现在,在现代 C++ 中,显式delete是一种代码味道,在某种程度上是delete this. 但是,我认为这种模式(创建一个对象来表示由回调链管理的一大块工作)基本上是一个好主意,或者至少不是一个坏主意。

所以我的问题是,我应该如何重写这种模式的实例来封装内存管理?

我认为不是一个好主意的一个选择将其存储Workershared_ptr: 中,从根本上说,这里不共享所有权,因此引用计数的开销是不必要的。此外,为了shared_ptr在回调中保持活动的副本,我需要继承 from enable_shared_from_this,并记住在 lambda之外调用它并将 捕获shared_ptr到回调中。如果我曾经直接使用写过简单的代码this,或者shared_from_this()在回调 lambda 中调用,那么该对象可能会被提前删除。

4

2 回答 2

3

我同意这delete this是一种代码气味,并且在较小程度delete上是其本身。但我认为这是延续传递风格的自然组成部分,(对我而言)这本身就是一种代码味道。

根本问题是这个 API 的设计假设了无限的控制流:它承认调用者对调用完成时发生的事情感兴趣,但通过任意复杂的回调而不是简单地从同步调用返回来表示完成。最好同步构造它,让调用者确定适当的并行化和内存管理机制:

class Worker {
 public:
  void DoWork(ArgT arg) {
    // Async1 is a mistake; fix it later.  For now, synchronize explicitly.
    Latch async_done(1);
    Async1(<args>, [&]() { async_done.count_down(); });
    async_done.await();

    Latch parallel_done(2);
    RunParallel([&]() { DoStuff(<args>); parallel_done.count_down(); });
    RunParallel([&]() { DoStuff(<other_args>); parallel_done.count_down(); };
    parallel_done.await();
  }
};

在调用方,它可能看起来像这样:

Latch latch(tasks.size());
for (auto& task : tasks) {
  RunParallel([=]() { DoWork(<args>); latch.count_down(); });
}
latch.await();

RunParallel 可以使用 std::thread 或您喜欢的任何其他机制来调度并行事件。

这种方法的优点是对象的生命周期要简单得多。ArgT 对象正好存在于 DoWork 调用的范围内。DoWork 的参数与包含它们的闭包一样长。这也使得向 DoWork 调用添加返回值(例如错误代码)变得更加容易:调用者只需从锁存器切换到线程安全队列并在它们完成时读取结果。

这种方法的缺点是它需要实际的线程,而不仅仅是 boost::asio::io_service。(例如,DoWork() 中的 RunParallel 调用不能阻塞等待来自调用方的 RunParallel 调用返回。)因此,您要么必须将代码构造成严格分层的线程池,要么必须允许潜在无限数量的线程。

于 2013-05-20T19:46:15.560 回答
0

一种选择是delete this这里不是代码异味。最多,它应该被包装到一个小库中,该库将检测是否所有的延续回调都在没有调用的情况下被销毁done_()

于 2013-05-19T01:46:07.307 回答