用 Seastar 的时候,常常需要推迟一个对象的析构。于是问题来了。

平时,我们这样写程序:

void scan(func_t&& f)
{
  Node root = get_root();
  return root.scan(std::move(f));
}

但是用 Seastar 的话,因为 get_root() 可能会阻塞,我们可能可以把代码写成下面这样:

seastar::future<> scan(func_t&& f)
{
  return get_root().then([f=std::move(f)](Node&& root) {
    return root.scan(std::move(f));
  });
}

那么既然 get_root() 是异步的,那么等到调用 then() 的时候,f 会不会已经析构了呢?我们是不是应该这么写?

seastar::future<> scan(func_t&& f)
{
  return seastar::do_with(std::move(f), [this](auto& f) {
    return get_root().then([&f](Node&& root) {
      return root.scan(f);
    });
  });
}

这里就是例子:

struct foo_t {
  foo_t& func(int i) {
    cout << "func(" << i << ")" << endl;
    return *this;
  }
};

int gen(int i) {
  cout << "gen(" << i << ")" << endl;
  return i;
}

int main()
{
  foo_t foo;
  foo.func(1).func(gen(2));
}

的输出是:

func(1)
gen(2)
func(2)

由此可知,gen(2) 是在 func(1) 返回之后才调用的。 而 get_root() 里面的代码如果是异步调用的话,可能在 scan() 返回的时候也还没有“完成”。因为异步调用返回的是一个 future ,如果 future state 当时还没有准备好,那么 .then(func) 则会把 func 包装成一个 task 等待调度。

但是从 C++ 的角度来说呢?简化版本的代码中,有这么个表达式

foo.func(1).func(gen(2));

它用来模拟 Seastar 里面 .then() 的调用,方便理解求值的先后顺序。很明显,这里的 AST 的树根是个函数调用。

Diagram

n4659 中, expr.call 一节说道

A function call is a postfix expression followed by parentheses containing a possibly empty, comma-separated list of initializer-clauses which constitute the arguments to the function.

接着标准规定了函数参数的求值顺序

The postfix-expression is sequenced before each expression in the expression-list and any default argument. The initialization of a parameter, including every associated value computation and side effect, is indeterminately sequenced with respect to that of any other parameter.

通俗易懂的话,就是:

When calling a function (whether or not the function is inline, and whether or not explicit function call syntax is used), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function.

所以,我们这个例子里面 .then() 有两个参数,在真正调用 .then() 之前,我们必须先对这两个参数求值。

  • 一个是 this,它的值由 get_root() 返回,为了得到这个参数必须对 get_root() 求值。

  • 另一个是一个 lambda 表达式,它的值由 capture list 和后面的函数体决定。但是请注意,要对这个表达式求值并不需要执行这个 lambda 表达式。它的值就是一个 lambda 表达式。

所以在调用 .then() 之前,f 的值就被稳妥地保存在第二个参数里面了,并且因为我们是 capture by move,所以第二个参数析构的时候,f 也会随之而去。我们并不需要为它专门做一个 seastar::do_with() 用智能指针保存它的值,延长其生命周期。

回到一开始的 foo_t 的那个例子,其实它有些许误导。我们按照结合律,可以把这个表达式拆成这么几个

Diagram

所以对第二个 .func() 求值,我们必须先对 foo.func(1)gen(2) 求值,当然它们的顺序不一定。然后再调用 foo.func(2)

但是和前文 scan() 的例子不一样,scan() 的第二个参数是个 lambda 表达式,为了对它求值,我们必须初始化 lambda 表达式中的 capture 列表。所以看上去好像有“写在后面的代码反而在之前执行了”的错觉。但是如果把语法关系理清楚,这个问题就迎刃而解了。