GCC-3.4.6源代码学习笔记(153)

5.13.1.2.2.3.  调用初始化方法

在下面默认的, flag_elide_constructor 1 ,这表示尽可能消减构造函数。构造函数(包括 operator= (const X&) ,如果定义了)是否可以被削减,依赖于它是否是平凡的。断言 TYPE_HAS_COMPLEX_INIT_REF 成立,如果该类类型具有非平凡拷贝构造函数;而断言 TYPE_HAS_COMPLEX_ASSIGN_REF 成立,如果该类类型具有非平凡的“ operator = (const X&) ”。

 

build_over_call (continue)

 

4534    if (warn_format )

4535      check_function_format (NULL, TYPE_ATTRIBUTES (TREE_TYPE (fn)),

4536                          converted_args);

4537

4538    /* Avoid actually calling copy constructors and copy assignment operators,

4539      if possible.  */

4540

4541    if (! flag_elide_constructors )

4542      /* Do things the hard way.  */ ;

4543    else if (TREE_VEC_LENGTH (convs) == 1

4544          && DECL_COPY_CONSTRUCTOR_P (fn))

4545    {

4546      tree targ;

4547      arg = skip_artificial_parms_for (fn, converted_args);

4548      arg = TREE_VALUE (arg);

4549

4550       /* Pull out the real argument, disregarding const-correctness.  */

4551       targ = arg;

4552      while (TREE_CODE (targ) == NOP_EXPR

4553             || TREE_CODE (targ) == NON_LVALUE_EXPR

4554             || TREE_CODE (targ) == CONVERT_EXPR)

4555        targ = TREE_OPERAND (targ, 0);

4556      if (TREE_CODE (targ) == ADDR_EXPR)

4557      {

4558        targ = TREE_OPERAND (targ, 0);

4559        if (!same_type_ignoring_top_level_qualifiers_p

4560             (TREE_TYPE (TREE_TYPE (arg)), TREE_TYPE (targ)))

4561          targ = NULL_TREE;

4562      }

4563      else

4564        targ = NULL_TREE;

4565

4566      if (targ)

4567        arg = targ;

4568      else

4569        arg = build_indirect_ref (arg, 0);

4570

4571      /* [class.copy]: the copy constructor is implicitly defined even if

4572        the implementation elided its use.  */

4573      if (TYPE_HAS_COMPLEX_INIT_REF (DECL_CONTEXT (fn)))

4574        mark_used (fn);

4575

4576      /* If we're creating a temp and we already have one, don't create a

4577        new one. If we're not creating a temp but we get one, use

4578        INIT_EXPR to collapse the temp into our target. Otherwise, if the

4579        ctor is trivial, do a bitwise copy with a simple TARGET_EXPR for a

4580        temp or an INIT_EXPR otherwise.  */

4581      if (integer_zerop (TREE_VALUE (args)))

4582      {

4583        if (TREE_CODE (arg) == TARGET_EXPR)

4584          return arg;

4585        else if (TYPE_HAS_TRIVIAL_INIT_REF (DECL_CONTEXT (fn)))

4586          return build_target_expr_with_type (arg, DECL_CONTEXT (fn));

4587      }

4588      else if (TREE_CODE (arg) == TARGET_EXPR

4589            || TYPE_HAS_TRIVIAL_INIT_REF (DECL_CONTEXT (fn)))

4590      {

4591        tree to = stabilize_reference

4592                   (build_indirect_ref (TREE_VALUE (args), 0));

4593

4594        val = build (INIT_EXPR, DECL_CONTEXT (fn), to, arg);

4595        return val;

4596      }

4597    }

 

根据【 3 】,条文 12.8 “拷贝类对象”,条款 2

一个非模板的构造函数,对于类 X 是一个拷贝构造函数,如果其第一个形参具有类型 X& const X& volatile X& const volatile X& ,并且要么没有其它形参,或其它形参都具有缺省实参( 8.3.6 )。 [ 例子: X::X(const X&) X::X(X&, int=1) 都是拷贝构造函数。

class X {

public :

X(int);

X(const X&, int = 1);

};

X a(1); // calls X(int);

X b(a, 0); // calls X(const X&, int);

X c = b; // calls X(const X&, int);

例子结束 ]

前面看到, 4543 行“ TREE_VEC_LENGTH (convs) ”的值实际上反映了函数的调用实参的数目(加上返回值);因为构造函数没有返回值,这里 1 表示调用中传给该拷贝构造函数的实参数目是 1

来到这里之前,实参经过了前面章节所述的转换,一般而言,可以直接为之构建一个 INDIRECT_REF 节点来代表引用( & ),除非先前已是引用。 4552 行的 WHILE 循环与 4556 行的 IF 语句就是判断这个引用,考虑下面的例子:

void f (char& t) {}

void g (int& b) { f (*(char*)b); }

int main () {

int b = 5;

g (b);

}

4560 行“ TREE_TYPE (TREE_TYPE (arg)) ”得到的类型是“ char ”,而“ TREE_TYPE (targ) ”则是“ int ”,这个引用类型“ int& b ”不可直接使用,需要构建“ char& ”的类型。

4573 行,如果 TYPE_HAS_COMPLEX_INIT_REF 成立,意味着对应的类定义了拷贝构造函数,或具有虚拟基类,或具有虚拟函数(显然,这个拷贝构造函数不会是平凡的),调用 mark_used 来标记这个拷贝构造函数为被使用,并且如果使用者没有声明,而是由编译器“人工”产生拷贝构造函数,合成它的定义。

4581 行, args 是调用的实参。它是应该 tree_list ,其中每个节点的 TREE_VALUE 域保存对应的实参。在上面的例子中,在“ X b(a, 0) ”中的“ TREE_VALUE (args) ”是隐含的 this 指针,它是 0 (满足 4581 行的条件);而在“ X c = b ”中,它是“ &c ”。对于前一个情形,需要构建新的对象(由于 C++ 语法的限制,它只能是函数内的临时对象);而对于后一个情形,我们可以使用‘ c ’作为 INIT_EXPR 的对象。在 TYPE_HAS_TRIVIAL_INIT_REF 成立,即可以使用按位拷贝来拷贝对象时,我们可以忽略拷贝构造函数。如果拷贝构造函数不能忽略,我们将去到下面的 4637 行来处理。

而在下面的 4599 行, copy_fn_p 返回非 0 值,如果 fn 是构造函数或重载的操作符“ = ”;而 4598 行的 DECL_OVERLOADED_OPERATOR_P 成立,如果 fn 是重载的操作符“ = ”;因此进入 4601 行的代码块的条件是: fn 是重载操作符“ = ”,但该类对象间的赋值可以通过按位拷贝完成(满足 4600 行条件)。

 

build_over_call (continue)

 

4598    else if (DECL_OVERLOADED_OPERATOR_P (fn) == NOP_EXPR

4599          && copy_fn_p (fn)

4600          && TYPE_HAS_TRIVIAL_ASSIGN_REF (DECL_CONTEXT (fn)))

4601    {

4602      tree to = stabilize_reference

4603                (build_indirect_ref (TREE_VALUE (converted_args), 0));

4604      tree type = TREE_TYPE (to);

4605      tree as_base = CLASSTYPE_AS_BASE (type);

4606

4607      arg = build_indirect_ref (TREE_VALUE (TREE_CHAIN (converted_args)), 0);

4608      if (tree_int_cst_equal (TYPE_SIZE (type), TYPE_SIZE (as_base)))

4609        val = build (MODIFY_EXPR, TREE_TYPE (to), to, arg);

4610      else

4611      {

4612        /* We must only copy the non-tail padding parts. Use

4613          CLASSTYPE_AS_BASE for the bitwise copy.  */

4614        tree to_ptr, arg_ptr, to_as_base, arg_as_base, base_ptr_type;

4615        tree save_to;

4616

4617        to_ptr = save_expr (build_unary_op (ADDR_EXPR, to, 0));

4618        arg_ptr = build_unary_op (ADDR_EXPR, arg, 0);

4619

4620        base_ptr_type = build_pointer_type (as_base);

4621        to_as_base = build_nop (base_ptr_type, to_ptr);

4622        to_as_base = build_indirect_ref (to_as_base, 0);

4623        arg_as_base = build_nop (base_ptr_type, arg_ptr);

4624        arg_as_base = build_indirect_ref (arg_as_base, 0);

4625

4626        save_to = build_indirect_ref (to_ptr, 0);

4627

4628        val = build (MODIFY_EXPR, as_base, to_as_base, arg_as_base);

4629        val = convert_to_void (val, NULL);

4630        val = build (COMPOUND_EXPR, type, val, save_to);

4631        TREE_NO_UNUSED_WARNING (val) = 1;

4632      }

4633       

4634       return val;

4635    }

 

4602 行从这个 this 指针获取了该类类型。在 4605 行的 CLASSTYPE_AS_BASE 返回了没有虚拟基类部分的类基干( class base );不过因为该拷贝构造函数是平凡的,这意味着这个类不包含任何虚拟基类。被返回的类基干应该与类的大小相同,除非这个类有填充字节(在 layout_class_type 中看到,类基干的大小不包括填充字节)。填充字节不应该纳入按位拷贝。

如果拷贝构造函数或重载的操作符“ = ”不是平凡的,则需要下面的处理来产生调用对应函数的代码。

 

build_over_call (continue)

 

4637    mark_used (fn);

4638

4639    if (DECL_VINDEX (fn) && (flags & LOOKUP_NONVIRTUAL) == 0)

4640    {

4641      tree t, *p = &TREE_VALUE (converted_args);

4642      tree binfo = lookup_base (TREE_TYPE (TREE_TYPE (*p)),

4643                            DECL_CONTEXT (fn),

4644                            ba_any, NULL);

4645      my_friendly_assert (binfo && binfo != error_mark_node, 20010730);

4646       

4647      *p = build_base_path (PLUS_EXPR, *p, binfo, 1);

4648      if (TREE_SIDE_EFFECTS (*p))

4649        *p = save_expr (*p);

4650      t = build_pointer_type (TREE_TYPE (fn));

4651      if (DECL_CONTEXT (fn) && TYPE_JAVA_INTERFACE (DECL_CONTEXT (fn)))

4652        fn = build_java_interface_fn_ref (fn, *p);

4653      else

4654        fn = build_vfn_ref (build_indirect_ref (*p, 0), DECL_VINDEX (fn));

4655      TREE_TYPE (fn) = t;

4656    }

4657    else if (DECL_INLINE (fn))

4658      fn = inline_conversion (fn);

4659    else

4660      fn = build_addr_func (fn);

4661

4662    return build_cxx_call (fn, args, converted_args);

4663 }

 

如果被调用的函数不是虚函数,就获取其地址并产生调用它的代码。对于内联函数,注意到内联函数,除了为引用或指针所援引的那些之外,不应该设置 TREE_ADDRESSABLE (这个位由 build_addr_func 设置),因为它总是在调用点展开。

 

1464 tree

1465 inline_conversion (tree exp)                                                                       in typeck.c

1466 {

1467    if (TREE_CODE (exp) == FUNCTION_DECL)

1468      exp = build1 (ADDR_EXPR, build_pointer_type (TREE_TYPE (exp)), exp);

1469

1470    return exp;

1471 }

 

而如果这个函数是虚函数,首先需要检查定义了这个函数的基类是否可以由 p 指定的类来访问。然后调整“ this ”指针指向这个基类。接着我们将从 vtable 中获取这个虚函数的地址。注意下面的 instance 是一个 INDIRECT_REF 节点。

 

470    tree

471    build_vfn_ref (tree instance, tree idx)                                                        in class.c

472    {

473      tree aref = build_vtbl_ref_1 (instance, idx);

474   

475      /* When using function descriptors, the address of the

476        vtable entry is treated as a function pointer.  */

477      if (TARGET_VTABLE_USES_DESCRIPTORS)

478        aref = build1 (NOP_EXPR, TREE_TYPE (aref),

479                     build_unary_op (ADDR_EXPR, aref, /*noconvert=*/ 1));

480   

481      return aref;

482    }

 

回忆下面 446 行的“ BINFO_VTABLE (binfo) ”返回基类 binfo vtable 。因为 binfo 来自最后派生类的 binfo 里,在前面我们已经看到,这样返回的 vtable 是该派生类 vtable 的一部分。而在 437 行, fixed_type_or_null 将返回 NULL ,因为 instance 是一个指针加法, fixed_type_or_null 并不能确定其动态类型。接着 build_vfield_ref 将为这个 vtable 构建 COMPONENT_REF 来代表对其的访问。

 

427    ic tree

428    build_vtbl_ref_1 (tree instance, tree idx)                                                    in class.c

429    {

430      tree aref;

431      tree vtbl = NULL_TREE;

432   

433      /* Try to figure out what a reference refers to, and

434        access its virtual function table directly.  */

435   

436      int cdtorp = 0;

437      tree fixed_type = fixed_type_or_null (instance, NULL, &cdtorp);

438   

439      tree basetype = non_reference (TREE_TYPE (instance));

440   

441      if (fixed_type && !cdtorp)

442      {

443        tree binfo = lookup_base (fixed_type, basetype,

444                              ba_ignore|ba_quiet, NULL);

445        if (binfo)

446          vtbl = BINFO_VTABLE (binfo);

447      }

448   

449      if (!vtbl)

450        vtbl = build_vfield_ref (instance, basetype);

451     

452      assemble_external (vtbl);

453   

454      aref = build_array_ref (vtbl, idx);

455   

456      return aref;

457    }

 

最后,访问 vtable 指定索引处,可以得到期望的虚函数地址,在上面 4662 行对其调用的代码由 build_cxx_call 产生。

5.13.1.2.3.        标准转换

在一个 IDENTITY_CONV BASE_CONV 中,如果拷贝构造函数必须是可访问的,即便不使用它, CHECK_COPY_CONSTRUCTOR_P 就是 true 。记得序列中的最后一个转换总是 IDENTITY_CONV

而如果我们看到一个具有二义性的转换,能来到这里,它是通过隐式转换进来的( implicit_conversion )。彼时,在该函数中调用 build_user_type_conversion_1 flags 实参为 LOOKUP_ONLYCONVERTING ,它不会输出二义性出错消息,因此以 LOOKUP_NORMAL 再调用一次,给出错误消息(这是转换发生点,在这里给出消息最准确)。

 

convert_like_real (continue)

 

4017      case IDENTITY_CONV:

4018        if (type_unknown_p (expr))

4019          expr = instantiate_type (totype, expr, tf_error | tf_warning);

4020         /* Convert a non-array constant variable to its underlying

4021          value, unless we are about to bind it to a reference, in

4022          which case we need to leave it as an lvalue.   */

4023        if (inner >= 0

4024           && TREE_CODE (TREE_TYPE (expr)) != ARRAY_TYPE)

4025          expr = decl_constant_value (expr);

4026        if (CHECK_COPY_CONSTRUCTOR_P (convs))

4027          check_constructor_callable (totype, expr);

4028       

4029        return expr;

4030      case AMBIG_CONV:

4031        /* Call build_user_type_conversion again for the error.  */

4032        return build_user_type_conversion

4033                   (totype, TREE_OPERAND (convs, 0), LOOKUP_NORMAL);

4034

4035      default :

4036        break ;

4037    };

 

注意到在上面, USER_CONV IDENTITY_CONV AMBIG_CONV 都在它们的 CASE 块后从函数返回;来到下面的代码,它将是其他的转换。注意 4039 行的递归,这是这个函数中我们可以深入序列的唯一的地方。显然, USER_CONV IDENTITY_CONV ,及 AMBIG_CONV 都是递归的出口。这是因为这个序列把其中的转换以反序链接(先目标,后源)。看到不是序列中所有的转换都会被访问到,例如,在用户定义转换序列中,总是在最后的 IDENTITY_CONV 将被跳过,因为递归在 USER_CONV 处终止。

 

convert_like_real (continue)

 

4039    expr = convert_like_real (TREE_OPERAND (convs, 0), expr, fn, argnum,

4040                         TREE_CODE (convs) == REF_BIND ? -1 : 1,

4041                          /*issue_conversion_warnings=*/ false);

4042    if (expr == error_mark_node)

4043      return error_mark_node;

4044

4045    switch (TREE_CODE (convs))

4046    {

4047      case RVALUE_CONV:

4048        if (! IS_AGGR_TYPE (totype))

4049           return expr;

4050        /* Else fall through.  */

4051      case BASE_CONV:

4052        if (TREE_CODE (convs) == BASE_CONV && !NEED_TEMPORARY_P (convs))

4053        {

4054          /* We are going to bind a reference directly to a base-class

4055            subobject of EXPR.  */

4056          if (CHECK_COPY_CONSTRUCTOR_P (convs))

4057            check_constructor_callable (TREE_TYPE (expr), expr);

4058          /* Build an expression for `*((base*) &expr)'.  */

4059          expr = build_unary_op (ADDR_EXPR, expr, 0);

4060          expr = perform_implicit_conversion (build_pointer_type (totype),

4061                                         expr);

4062          expr = build_indirect_ref (expr, "implicit conversion");

4063           return expr;

4064        }

4065

4066        /* Copy-initialization where the cv-unqualified version of the source

4067          type is the same class as, or a derived class of, the class of the

4068          destination [is treated as direct-initialization]. [dcl.init] */

4069        expr = build_temp (expr, totype, LOOKUP_NORMAL|LOOKUP_ONLYCONVERTING,

4070                        &diagnostic_fn);

4071        if (diagnostic_fn && fn)

4072          diagnostic_fn ("  initializing argument %P of `%D'", argnum, fn);

4073        return build_cplus_new (totype, expr);

4074

4075      case REF_BIND:

4076      {

4077        tree ref_type = totype;

4078

4079        /* If necessary, create a temporary.  */

4080        if (NEED_TEMPORARY_P (convs) || !lvalue_p (expr))

4081        {

4082          tree type = TREE_TYPE (TREE_OPERAND (convs, 0));

4083          cp_lvalue_kind lvalue = real_lvalue_p (expr);

4084

4085          if (!CP_TYPE_CONST_NON_VOLATILE_P (TREE_TYPE (ref_type)))

4086          {

4087              /* If the reference is volatile or non-const, we

4088              cannot create a temporary.  */

4089            if (lvalue & clk_bitfield)

4090              error ("cannot bind bitfield `%E' to `%T'",

4091                    expr, ref_type);

4092             else if (lvalue & clk_packed)

4093              error ("cannot bind packed field `%E' to `%T'",

4094                    expr, ref_type);

4095            else

4096              error ("cannot bind rvalue `%E' to `%T'", expr, ref_type);

4097            return error_mark_node;

4098          }

4099          /* If the source is a packed field, and we must use a copy

4100            constructor, then building the target expr will require

4101            binding the field to the reference parameter to the

4102             copy constructor, and we'll end up with an infinite

4103            loop. If we can use a bitwise copy, then we'll be

4104            OK.  */

4105          if ((lvalue & clk_packed)

4106             && CLASS_TYPE_P (type)

4107             && !TYPE_HAS_TRIVIAL_INIT_REF (type))

4108          {

4109            error ("cannot bind packed field `%E' to `%T'",

4110                  expr, ref_type);

4111            return error_mark_node;

4112          }

4113          expr = build_target_expr_with_type (expr, type);

4114        }

4115

4116         /* Take the address of the thing to which we will bind the

4117          reference.  */

4118        expr = build_unary_op (ADDR_EXPR, expr, 1);

4119        if (expr == error_mark_node)

4120          return error_mark_node;

4121

4122        /* Convert it to a pointer to the type referred to by the

4123          reference. This will adjust the pointer if a derived to

4124          base conversion is being performed.  */

4125        expr = cp_convert (build_pointer_type (TREE_TYPE (ref_type)),

4126                        expr);

4127        /* Convert the pointer to the desired reference type.  */

4128        return build_nop (ref_type, expr);

4129      }

4130

4131      case LVALUE_CONV:

4132        return decay_conversion (expr);

4133

4134      case QUAL_CONV:

4135        /* Warn about deprecated conversion if appropriate.  */

4136        string_conv_p (totype, expr, 1);

4137        break ;

4138       

4139      default :

4140        break ;

4141    }

4142    return ocp_convert (totype, expr, CONV_IMPLICIT,

4143                     LOOKUP_NORMAL|LOOKUP_NO_CONVERSION);

4144 }

 

显然,这里 LVALUE_CONV RVALUE_CONV REF_BIND QUAL_CONV ,及 BASE_CONV 则按照从源类型到目标类型的转换步骤执行。而 PTR_CONV QUAL_CONV ,及 PMEM_CONV 则是直接由下面的函数处理。

下面的参数 convtype 是一个比特位的集合,其中位 CONV_FORCE_TEMP 表示当转换到同一个聚合类型时,要求一个新的临时对象(不过在我们这里的调用上下文中,这个参数是 CONV_IMPLICIT ,它表示执行隐式转换)。这个函数把通过参数 expr 传入的表达式转换到由参数 type 指定的类型。

 

614    tree

615    ocp_convert (tree type, tree expr, int convtype, int flags)                                          in cvt.c

616    {

617      tree e = expr;

618      enum tree_code code = TREE_CODE (type);

619   

620      if (error_operand_p (e) || type == error_mark_node)

621        return error_mark_node;

622   

623      complete_type (type);

624      complete_type (TREE_TYPE (expr));

625   

626      e = decl_constant_value (e);

627   

628      if (IS_AGGR_TYPE (type) && (convtype & CONV_FORCE_TEMP)

629           /* Some internal structures (vtable_entry_type, sigtbl_ptr_type)

630             don't go through finish_struct, so they don't have the synthesized

631             constructors. So don't force a temporary.  */

632          && TYPE_HAS_CONSTRUCTOR (type))

633      /* We need a new temporary; don't take this shortcut.  */ ;

634      else if (TYPE_MAIN_VARIANT (type) == TYPE_MAIN_VARIANT (TREE_TYPE (e)))

635      {

636        if (same_type_p (type, TREE_TYPE (e)))

637          /* The call to fold will not always remove the NOP_EXPR as

638             might be expected, since if one of the types is a typedef;

639             the comparison in fold is just equality of pointers, not a

640             call to comptypes. We don't call fold in this case because

641             that can result in infinite recursion; fold will call

642             convert, which will call ocp_convert, etc.  */

643          return e;

644        /* For complex data types, we need to perform componentwise

645          conversion.  */

646        else if (TREE_CODE (type) == COMPLEX_TYPE)

647          return fold (convert_to_complex (type, e));

648        else if (TREE_CODE (e) == TARGET_EXPR)

649        {

650          /* Don't build a NOP_EXPR of class type. Instead, change the

651             type of the temporary. Only allow this for cv-qual changes,

652             though.  */

653          if (!same_type_p (TYPE_MAIN_VARIANT (TREE_TYPE (e)),

654                         TYPE_MAIN_VARIANT (type)))

655            abort ();

656          TREE_TYPE (e) = TREE_TYPE (TARGET_EXPR_SLOT (e)) = type;

657          return e;

658        }

659        else if (TREE_ADDRESSABLE (type))

660          /* We shouldn't be treating objects of ADDRESSABLE type as rvalues.  */

661          abort ();

662        else

663          return fold (build1 (NOP_EXPR, type, e));

664      }

665   

666      if (code == VOID_TYPE && (convtype & CONV_STATIC))

667      {

668        e = convert_to_void (e, /*implicit=*/ NULL);

669        return e;

670      }

671   

672      if (INTEGRAL_CODE_P (code))

673      {

674        tree intype = TREE_TYPE (e);

675        /* enum = enum, enum = int, enum = float, (enum)pointer are all

676          errors.  */

677        if (TREE_CODE (type) == ENUMERAL_TYPE

678           && ((ARITHMETIC_TYPE_P (intype) && ! (convtype & CONV_STATIC))

679                || (TREE_CODE (intype) == POINTER_TYPE)))

680        {

681          pedwarn ("conversion from `%#T' to `%#T'", intype, type);

682   

683          if (flag_pedantic_errors )

684            return error_mark_node;

685        }

686        if (IS_AGGR_TYPE (intype))

687        {

688          tree rval;

689          rval = build_type_conversion (type, e);

690          if (rval)

691            return rval;

692          if (flags & LOOKUP_COMPLAIN)

693            error ("`%#T' used where a `%T' was expected", intype, type);

694          if (flags & LOOKUP_SPECULATIVELY)

695            return NULL_TREE;

696          return error_mark_node;

697        }

698        if (code == BOOLEAN_TYPE)

699          return cp_truthvalue_conversion (e);

700   

701        return fold (convert_to_integer (type, e));

702      }

703      if (POINTER_TYPE_P (type) || TYPE_PTR_TO_MEMBER_P (type))

704        return fold (cp_convert_to_pointer (type, e, false));

 

上面,从 672 702 行处理到整型的转换。这个函数将被用于 static_cast const_cast ,及 reinterpret_cast ,它们分别为位 CONV_STATIC CONV_CONST CONV_REINTERPRET 表示。在上面的代码中,可以清楚地看到这些转换的限制。

 

你可能感兴趣的:(tree,null,Build,reference,Constructor,fold)