简析 gcc 中 lambda 表达式的实现

Published: April 27 2014

昨天下午搞了 std::function, 晚上在想要不顺便把 lambda 原理大概看一下好了,顺便了解一下 gcc。于是今天下午就搞起~

prerequisite 1. c++ lambda 2. 编译基础知识

代码来源于 gcc trunk,如果图方便可以直接到 github 下一个 zip 回来,或者直接在线看。

首先 grep lambda

发现在这里有粗线。 gcc/cp/parser.c line 1965

static tree cp_parser_lambda_expression
  (cp_parser *);
static void cp_parser_lambda_introducer
  (cp_parser *, tree);
static bool cp_parser_lambda_declarator_opt
  (cp_parser *, tree);
static void cp_parser_lambda_body
  (cp_parser *, tree);

line 3078

/* This is good for lambda expression capture-lists. */
     CPP_OPEN_SQUARE:
  ++square_depth;
  break;
     CPP_CLOSE_SQUARE:
  if (!square_depth--)
    return 0;
  break;

看来这里是在做 [] 的事情,也就是 lambda 的 capture list,继续往下看。

line 4316

    case CPP_OPEN_SQUARE:
      if (c_dialect_objc ())
        /* We have an Objective-C++ message. */
        return cp_parser_objc_expression (parser);
      {
        tree lam = cp_parser_lambda_expression (parser);
        /* Don't warn about a failed tentative parse. */
        if (cp_parser_error_occurred (parser))
          return error_mark_node;
        maybe_warn_cpp0x (CPP0X_LAMBDA_EXPR);
        return lam;
      }

这里应该是 parse lambda 的地方。网上看,我们是在 cp_parser_primary_expression 这个函数内 也就是说 lambda 是 primary expression 的一种。

gcc/c-family/c-common.h line 466

#define c_dialect_objc() ((c_language & clk_objc) != 0)

也就是判断是不是 objective-C。那我们不管他,继续向下看 cpp parse 的部分。

line 8717

static tree
cp_parser_lambda_expression (cp_parser* parser)
  tree lambda_expr = build_lambda_expr ();
  tree type;
  bool ok = true;
  cp_token *token = cp_lexer_peek_token (parser->lexer);
  LAMBDA_EXPR_LOCATION (lambda_expr) = token->location;
  if (cp_unevaluated_operand)
    {
      if (!token->error_reported)
        {
          error_at (LAMBDA_EXPR_LOCATION (lambda_expr),
                    "lambda-expression in unevaluated context");
          token->error_reported = true;
        }
      ok = false;
    }

终于我们找到了他! 我们一步一步慢慢来看。首先是 build_lambda_expr(), 应该是构建 parse tree 中的 lambda 对象。

gcc/cp/lambda.c line 37

tree
build_lambda_expr (void)
{
  tree lambda = make_node (LAMBDA_EXPR);
  LAMBDA_EXPR_DEFAULT_CAPTURE_MODE (lambda) = CPLD_NONE;
  LAMBDA_EXPR_CAPTURE_LIST (lambda) = NULL_TREE;
  LAMBDA_EXPR_THIS_CAPTURE (lambda) = NULL_TREE;
  LAMBDA_EXPR_PENDING_PROXIES (lambda) = NULL;
  LAMBDA_EXPR_RETURN_TYPE (lambda) = NULL_TREE;
  LAMBDA_EXPR_MUTABLE_P (lambda) = false;
  return lambda;
}

mutable_p 应该就是 lambda 带不带 mutable 了,capture_list, this_capture 这些都可以意会到。 大概可以看到 lambda 表达式所需要的几个东东,capture mode, pending proxies 这几个一眼看去不知道是干嘛的,我们接着刚才的 parse 过程看好了。

首先确定 lambda expr 在源文件中的 location。

接着做 cp_unevaluated_operand 判断,如果不为 0 且之前没有报错的话,现在报错。

line 258

/* Nonzero if we are parsing an unevaluated operand: an operand to
   sizeof, typeof, or alignof. */
int cp_unevaluated_operand;

也就是说 lambda 是不可以放在 sizeof, typeof, alignof 这几个里面的,我们随便写个程序试一下,当然会报错。

~ $ g++ -std=c++11 test.cpp test.cpp: In function ‘int main()’: test.cpp:47:20: error: lambda-expression in unevaluated context cout << sizeof([]() { return 1; }) << endl;

The evaluation of a lambda-expression results in a prvalue temporary (12.2). This temporary is called the closure object. A lambda-expression shall not appear in an unevaluated operand (Clause 5). [ Note: A closure object behaves like a function object (20.8).—end note ] (emphasis mine)

错误处理之后,push_deferring_access_checks 推迟 access check ,(gcc/cp/semantics.c)因为我们现在还不知道 capture list呢。

  /* We may be in the middle of deferred access check. Disable
     it now. */
  push_deferring_access_checks (dk_no_deferred);

接着开始 parse introducer

cp_parser_lambda_introducer (parser, lambda_expr);

line 8833

  /* Need commas after the first capture. */
  bool first = true;
  /* Eat the leading `['. */
  cp_parser_require (parser, CPP_OPEN_SQUARE, RT_OPEN_SQUARE);
  /* Record default capture mode. "[&" "[=" "[&," "[=," */
  if (cp_lexer_next_token_is (parser->lexer, CPP_AND)
      && cp_lexer_peek_nth_token (parser->lexer, 2)->type != CPP_NAME)
    LAMBDA_EXPR_DEFAULT_CAPTURE_MODE (lambda_expr) = CPLD_REFERENCE;
  else if (cp_lexer_next_token_is (parser->lexer, CPP_EQ))
    LAMBDA_EXPR_DEFAULT_CAPTURE_MODE (lambda_expr) = CPLD_COPY;
  if (LAMBDA_EXPR_DEFAULT_CAPTURE_MODE (lambda_expr) != CPLD_NONE)
    {
      cp_lexer_consume_token (parser->lexer);
      first = false;
    }

原来 capture mode 就是指 &, = 这些 capture 的方式。 上面都是在做一些 cosume token 的工作,我们接着看。

  while (cp_lexer_next_token_is_not (parser->lexer, CPP_CLOSE_SQUARE))

恩要开始 parse capture list 了。 细节就不追求,我们看几个关键的。

enum capture_kind_type
{
  BY_COPY,
  BY_REFERENCE
};
 /* Possibly capture `this'. */
if (cp_lexer_next_token_is_keyword (parser->lexer, RID_THIS))
  {
    location_t loc = cp_lexer_peek_token (parser->lexer)->location;
    if (LAMBDA_EXPR_DEFAULT_CAPTURE_MODE (lambda_expr) == CPLD_COPY)
      pedwarn (loc, 0, "explicit by-copy capture of %<this%> redundant "
               "with by-copy capture default");
    cp_lexer_consume_token (parser->lexer);
    add_capture (lambda_expr,
                 /*id=*/this_identifier,
                 /*initializer=*/finish_this_expr(),
                 /*by_reference_p=*/false,
                 explicit_init_p);
    continue;
  }

这是 capture this 指针的情况。注意到在后面还有

add_capture (lambda_expr,
             capture_id,
             capture_init_expr,
             /*by_reference_p=*/capture_kind == BY_REFERENCE,
             explicit_init_p);

看来问题的关键是看 add_capture 是怎么做的。

lambda.c line 439

tree
add_capture (tree lambda, tree id, tree orig_init, bool by_reference_p,
             bool explicit_init_p)

参数分别是 lambda_expr, capture 到的 identifier,initializer, 是否按引用传递。 最后一个 explicit_init_p,可以看到在 parse 的 while loop 里面,一开始是被设成 false 的。

什么时候是 true 呢,在 whilte loop 的中间有这样一段:

line 8934

/* Find the initializer for this capture. */
if (cp_lexer_next_token_is (parser->lexer, CPP_EQ)
    || cp_lexer_next_token_is (parser->lexer, CPP_OPEN_PAREN)
 cp_lexer_next_token_is (parser->lexer, CPP_OPEN_BRACE))
  {
    bool direct, non_constant;
    /* An explicit initializer exists. */
    if (cxx_dialect < cxx1y)
      pedwarn (input_location, 0,
               "lambda capture initializers "
               "only available with -std=c++1y or -std=gnu++1y");
    capture_init_expr = cp_parser_initializer (parser, &direct,
                                               &non_constant);
    explicit_init_p = true;
    if (capture_init_expr == NULL_TREE)
      {
        error ("empty initializer for lambda init-capture");
        capture_init_expr = error_mark_node;
      }
  }
else

也就是说,explicit_init_p 是说是否进行初始化,因为可能lambda catpure 到了一个在上面进行构造的对象。 这是 c++1y 的特性喔。 比如说

auto lambda = [value = 1] {return value;};
 
auto ptr = std::make_unique<int>(10); // See below for std::make_unique auto lambda = [ptr = std::move(ptr)] {return *ptr;};

explicit_init_p 也就是干这个用的,当存在 initializer 时为 true。 不过,看到 /initializer=/finish_this_expr(), this 的 initializer 有什么猫腻么? 跟进去看一下。

semantics.c line 2426

tree
finish_this_expr (void)
{
  tree result;
  if (current_class_ptr)
    {
      tree type = TREE_TYPE (current_class_ref);
      /* In a lambda expression, 'this' refers to the captured 'this'. */
      if (LAMBDA_TYPE_P (type))
        result = lambda_expr_this_capture (CLASSTYPE_LAMBDA_EXPR (type));
      else
        result = current_class_ptr;
    }
  else if (current_function_decl
           && DECL_STATIC_FUNCTION_P (current_function_decl))
    {

竟然有对 lambda 表达式的特殊处理,来看 lambda_expr_this_capture。

lambda.c line 626

/* Return the capture pertaining to a use of 'this' in LAMBDA, in the form of an
   INDIRECT_REF, possibly adding it through default capturing. */
tree
lambda_expr_this_capture (tree lambda)

这里的代码有些复杂,暂时不仔细追求,以免影响了理解 lambda 的大局。不过可以看一下注释

/* If we are in a lambda function, we can move out until we hit:
     1. a non-lambda function or NSDMI,
     2. a lambda function capturing 'this', or
     3. a non-default capturing lambda function. */

原来他是在费尽心思做这种事情。 我们现在还在 finish_this_expr -> lambda_expr_this_capture 上。 不过已经大概清楚这一步是想找到适合的 this。

刚才是 this 指针的 initializer (orig_init 参数), 那普通的变量 capture 这个要穿什么呢?

之前我们有看到 c++1y 的奇妙 lambda initializer expression,不过对于一般情况呢? 接着上面 whilte loop 中的 else

parser.c line 8943

          /* Turn the identifier into an id-expression. */
          capture_init_expr
            = cp_parser_lookup_name_simple (parser, capture_id,
                                            capture_token->location);

开始做 name lookup 了,看看这个东西之前有没有定义过。接下来是一些关于 capture 到的 id 的一些性质上的判断。

if (capture_init_expr == error_mark_node)
  {
    unqualified_name_lookup_error (capture_id);
    continue;
  }
else if (DECL_P (capture_init_expr)
         && (!VAR_P (capture_init_expr)
             && TREE_CODE (capture_init_expr) != PARM_DECL))
  {
    error_at (capture_token->location,
              "capture of non-variable %qD ",
              capture_init_expr);
    inform (0, "%q+#D declared here", capture_init_expr);
    continue;
  }
if (VAR_P (capture_init_expr)
    && decl_storage_duration (capture_init_expr) != dk_auto)
  {
    if (pedwarn (capture_token->location, 0, "capture of variable "
                 "%qD with non-automatic storage duration",
                 capture_init_expr))
      inform (0, "%q+#D declared here", capture_init_expr);
    continue;
  }

最后来到这里,

capture_init_expr
            = finish_id_expression
                (capture_id,
                 capture_init_expr,
                 parser->scope,
                 &idk,
                 /*integral_constant_expression_p=*/false,
                 /*allow_non_integral_constant_expression_p=*/false,
                 /*non_integral_constant_expression_p=*/NULL,
                 /*template_p=*/false,
                 /*done=*/true,
                 /*address_p=*/false,
                 /*template_arg_p=*/false,
                 &error_msg,
                 capture_token->location);

再往后就是一些简单的逻辑,暂且不看。记得刚才也有 finish_this_expr,而这里有一个 finish_id_expression。 看来一个 identifier 总是要 finish 一下,跟他之前的声明做 binding。来看一下 finish_id_expression

semantics.c 3093

具体的代码就不贴了。以免脱离主题。我们还是回到 add_capture 上来。 基于相似的理由,add_capture 的代码还是不贴了(太长)。大致的逻辑是根据传进来的参数构建 capture list 的 parse tree。

似乎在 cp_parser_lambda_introducer 上耗费了太长的时间,一直都没进入真正的主题。 不过怎么说,也算熟悉了一下 gcc 的套路(顺便吐槽一下模块真是太乱了)

parser.c line 8741

  /* We may be in the middle of deferred access check. Disable
     it now. */
  push_deferring_access_checks (dk_no_deferred);
  cp_parser_lambda_introducer (parser, lambda_expr);
  type = begin_lambda_type (lambda_expr);
  if (type == error_mark_node)
    return error_mark_node;
  record_lambda_scope (lambda_expr);
  /* Do this again now that LAMBDA_EXPR_EXTRA_SCOPE is set. */
  determine_visibility (TYPE_NAME (type));
  /* Now that we've started the type, add the capture fields for any
     explicit captures. */
  register_capture_members (LAMBDA_EXPR_CAPTURE_LIST (lambda_expr));

唉,慢慢长征,我们刚刚只是过了一下 cp_parser_lambda_introducer。不过对于 lambda 来说,introducer 之后就是 function,function parse 的过程我们就不必关心了。所以路程应该已经走了一半。

lambda.c line 127

begin_lambda_type (tree lambda)
{
  tree type;
  {
    /* Unique name. This is just like an unnamed class, but we cannot use
       make_anon_name because of certain checks against TYPE_ANONYMOUS_P. */
    tree name;
    name = make_lambda_name ();
    /* Create the new RECORD_TYPE for this lambda. */
    type = xref_tag (/*tag_code=*/record_type,
                     name,
                     /*scope=*/ts_lambda,
                     /*template_header_p=*/false);
    if (type == error_mark_node)
      return error_mark_node;
  }
  /* Designate it as a struct so that we can use aggregate initialization. */
  CLASSTYPE_DECLARED_CLASS (type) = false;
  /* Cross-reference the expression and the type. */
  LAMBDA_EXPR_CLOSURE (lambda) = type;
  CLASSTYPE_LAMBDA_EXPR (type) = lambda;
  /* Clear base types. */
  xref_basetypes (type, /*bases=*/NULL_TREE);
  /* Start the class. */
  type = begin_class_definition (type);
  return type;
}

这里我们给 lambda 一个具体的类型,怎样的类型呢? 就在 xref_tag 那里,跟进去看

decl.c line 12183

tree
xref_tag (enum tag_types tag_code, tree name,
          tag_scope scope, bool template_header_p)
{
  tree ret;
  bool subtime;
  subtime = timevar_cond_start (TV_NAME_LOOKUP);
  ret = xref_tag_1 (tag_code, name, scope, template_header_p);
  timevar_cond_stop (TV_NAME_LOOKUP, subtime);
  return ret;
}

又是代理,我们继续跟。

decl.c line 12038

static tree
xref_tag_1 (enum tag_types tag_code, tree name,
            tag_scope orig_scope, bool template_header_p)
{
  enum tree_code code;
  tree t;
  tree context = NULL_TREE;
  tag_scope scope;
  gcc_assert (identifier_p (name));
  switch (tag_code)
    {
    case record_type:
    case class_type:
      code = RECORD_TYPE;
      break;

真是是 struct ! 在 gcc 内部,struct 被叫成了 record。于是我们已经知道了 lambda 究竟是怎样的东西~ 当然在后面,我们会找到对应的

 type = finish_struct (type, /*attributes=*/NULL_TREE); 

继续看关键的 register_capture_members

lambda.c line 567

/* Register all the capture members on the list CAPTURES, which is the
   LAMBDA_EXPR_CAPTURE_LIST for the lambda after the introducer. */
void
register_capture_members (tree captures)
{
  if (captures == NULL_TREE)
    return;
  register_capture_members (TREE_CHAIN (captures));
  tree field = TREE_PURPOSE (captures);
  if (PACK_EXPANSION_P (field))
    field = PACK_EXPANSION_PATTERN (field);
  /* We set this in add_capture to avoid duplicates. */
  IDENTIFIER_MARKED (DECL_NAME (field)) = false;
  finish_member_declaration (field);
}

递归注册所有 capture 成员,因为递归搞反了顺序,后面还有相应的 reverse 修正。 而这一步把 capture list 的各个东东都注册成了 record parse tree 的 node,也就是说,成为了 lambda 这个 struct 的 member。

继续往后看,忽略中间的细节。

  pop_deferring_access_checks ();
  /* This field is only used during parsing of the lambda. */
  LAMBDA_EXPR_THIS_CAPTURE (lambda_expr) = NULL_TREE;
  /* This lambda shouldn't have any proxies left at this point. */
  gcc_assert (LAMBDA_EXPR_PENDING_PROXIES (lambda_expr) == NULL);
  /* And now that we're done, push proxies for an enclosing lambda. */
  insert_pending_capture_proxies ();
  if (ok)
    return build_lambda_object (lambda_expr);
  else
    return error_mark_node;

注意到 insert_pending_capture_proxies,这应该是对应之前我们所不知道的 LAMBDA_EXPR_PENDING_PROXIES 。 还是在 lambda.c 里面

/* We've just finished processing a lambda; if the containing scope is also
   a lambda, insert any capture proxies that were created while processing
   the nested lambda. */
void
insert_pending_capture_proxies (void)
{
  tree lam;
  vec<tree, va_gc> *proxies;
  unsigned i;
  if (!current_function_decl || !LAMBDA_FUNCTION_P (current_function_decl))
    return;
  lam = CLASSTYPE_LAMBDA_EXPR (DECL_CONTEXT (current_function_decl));
  proxies = LAMBDA_EXPR_PENDING_PROXIES (lam);
  for (i = 0; i < vec_safe_length (proxies); ++i)
    {
      tree var = (*proxies)[i];
      insert_capture_proxy (var);
    }
  release_tree_vector (LAMBDA_EXPR_PENDING_PROXIES (lam));
  LAMBDA_EXPR_PENDING_PROXIES (lam) = NULL;
}

再看具体做事的 insert_capture_proxy

/* VAR is a capture proxy created by build_capture_proxy; add it to the
   current function, which is the operator() for the appropriate lambda. */
void
insert_capture_proxy (tree var)
{
  cp_binding_level *b;
  tree stmt_list;
  /* Put the capture proxy in the extra body block so that it won't clash
     with a later local variable. */
  b = current_binding_level;
  for (;;)
    {
      cp_binding_level *n = b->level_chain;
      if (n->kind == sk_function_parms)
        break;
      b = n;
    }
  pushdecl_with_scope (var, b, false);
  /* And put a DECL_EXPR in the STATEMENT_LIST for the same block. */
  var = build_stmt (DECL_SOURCE_LOCATION (var), DECL_EXPR, var);
  stmt_list = (*stmt_list_stack)[1];
  gcc_assert (stmt_list);
  append_to_statement_list_force (var, &stmt_list);
}

原来是往里面放了一个 declaration statement,声明变量。似乎有些搞不懂这个 proxy 是做什么的,难道是之前我们跳跃的太快漏掉了什么?

发现还存在以下的调用关系 add_capture -> build_capture_proxy -> insert_capture_proxy 。

因为已经看的累了,就一遍看注释一遍 yy 吧。

/* MEMBER is a capture field in a lambda closure class. Now that we're
   inside the operator(), build a placeholder var for future lookups and
   debugging. */
tree
build_capture_proxy (tree member)

原来 proxy 就是对 capture list 中成员的 proxy。而为什么有 pending 的呢? 就是因为存在 capture & = 这样的情况,要在之后才能 resolve。

最后看 build_lambda_object,已经是在做一些收尾工作,给 lamabda 添加 ctor 等等 。。。

至此,终于简要的过了一遍 lambda 的实现。lambda 功能都是在前端 parser 时候做的(甜甜的语法糖),索性我们不用看后端的代码~ 其实看得有点烂尾,因为 add_capture 里面的东西还是没有仔细研究,看这个代码真的很累很累 = =

总结一下~

  1. 对于一个大项目来说,看代码并不是一件难事。grep 在手天下我有。只要抓住某条线不放总能找到蛛丝马迹。
  2. 如果想深入了解每一个细节还是有难度的,主要还是精力问题。所以看代码的时候要进得去出的来,千万别抱死在某个函数上。
  3. C 的代码确实很难看,很难看,还好 gcc 的注释够给力。
  4. 一句话总结 lambda,带 operator() 的 struct。
> 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 <
blog comments powered by Disqus