使用emplace_back的new initializer expression list treated as compound expression提示看聚合初始化和parameter pack

测试代码

使用emplace_back可以避免不必要的构造和拷贝,而是直接在向量的内存位置执行construct进行构造,代码看起来也更加简洁。
但是在使用的时候,会发现有一些和直观不太对应的情况。例如,在下面的例子中,使用new的时候,只有花括号可以通过编译,而同样的emplace_back就不行。

tsecer@harry$ cat -n default.new.agg.init.cpp
     1  #include <vector>
     2
     3  struct tsecer
     4  {
     5      int x;
     6      int y;
     7  };
     8
     9  int harry()
    10  {
    11      new tsecer{1,2};
    12      new tsecer(1,2);
    13
    14      std::vector<tsecer> va;
    15      va.emplace_back(1, 2);
    16      va.emplace_back({1, 2});
    17  }
    18
tsecer@harry$ gcc -c default.new.agg.init.cpp 
default.new.agg.init.cpp: In function 'int harry()':
default.new.agg.init.cpp:12:19: error: new initializer expression list treated as compound expression [-fpermissive]
     new tsecer(1,2);
                   ^
default.new.agg.init.cpp:12:19: error: no matching function for call to 'tsecer::tsecer(int)'
default.new.agg.init.cpp:3:8: note: candidate: tsecer::tsecer()
 struct tsecer
        ^~~~~~
default.new.agg.init.cpp:3:8: note:   candidate expects 0 arguments, 1 provided
default.new.agg.init.cpp:3:8: note: candidate: constexpr tsecer::tsecer(const tsecer&)
default.new.agg.init.cpp:3:8: note:   no known conversion for argument 1 from 'int' to 'const tsecer&'
default.new.agg.init.cpp:3:8: note: candidate: constexpr tsecer::tsecer(tsecer&&)
default.new.agg.init.cpp:3:8: note:   no known conversion for argument 1 from 'int' to 'tsecer&&'
default.new.agg.init.cpp:16:27: error: no matching function for call to 'std::vector<tsecer>::emplace_back(<brace-enclosed initializer list>)'
     va.emplace_back({1, 2});
                           ^
In file included from /usr/include/c++/7/vector:69:0,
                 from default.new.agg.init.cpp:1:
/usr/include/c++/7/bits/vector.tcc:95:7: note: candidate: void std::vector<_Tp, _Alloc>::emplace_back(_Args&& ...) [with _Args = {}; _Tp = tsecer; _Alloc = std::allocator<tsecer>]
       vector<_Tp, _Alloc>::
       ^~~~~~~~~~~~~~~~~~~
/usr/include/c++/7/bits/vector.tcc:95:7: note:   candidate expects 0 arguments, 1 provided
In file included from /usr/include/c++/7/x86_64-redhat-linux/bits/c++allocator.h:33:0,
                 from /usr/include/c++/7/bits/allocator.h:46,
                 from /usr/include/c++/7/vector:61,
                 from default.new.agg.init.cpp:1:
/usr/include/c++/7/ext/new_allocator.h: In instantiation of 'void __gnu_cxx::new_allocator<_Tp>::construct(_Up*, _Args&& ...) [with _Up = tsecer; _Args = {int, int}; _Tp = tsecer]':
/usr/include/c++/7/bits/alloc_traits.h:475:4:   required from 'static void std::allocator_traits<std::allocator<_Tp1> >::construct(std::allocator_traits<std::allocator<_Tp1> >::allocator_type&, _Up*, _Args&& ...) [with _Up = tsecer; _Args = {int, int}; _Tp = tsecer; std::allocator_traits<std::allocator<_Tp1> >::allocator_type = std::allocator<tsecer>]'
/usr/include/c++/7/bits/vector.tcc:100:30:   required from 'void std::vector<_Tp, _Alloc>::emplace_back(_Args&& ...) [with _Args = {int, int}; _Tp = tsecer; _Alloc = std::allocator<tsecer>]'
default.new.agg.init.cpp:15:25:   required from here
/usr/include/c++/7/ext/new_allocator.h:136:4: error: new initializer expression list treated as compound expression [-fpermissive]
  { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/7/ext/new_allocator.h:136:4: error: no matching function for call to 'tsecer::tsecer(int)'
default.new.agg.init.cpp:3:8: note: candidate: tsecer::tsecer()
 struct tsecer
        ^~~~~~
default.new.agg.init.cpp:3:8: note:   candidate expects 0 arguments, 1 provided
default.new.agg.init.cpp:3:8: note: candidate: constexpr tsecer::tsecer(const tsecer&)
default.new.agg.init.cpp:3:8: note:   no known conversion for argument 1 from 'int' to 'const tsecer&'
default.new.agg.init.cpp:3:8: note: candidate: constexpr tsecer::tsecer(tsecer&&)
default.new.agg.init.cpp:3:8: note:   no known conversion for argument 1 from 'int' to 'tsecer&&'
tsecer@harry$ 

聚合类型

C++定义了aggregate类型,从这个说明来看,感觉大致可以理解为传统的C语言中的上struct结构:没有构造函数,没有virtual函数和virtual继承。
它们的初始化也和C中的结构初始化相同,也是通过花括号初始化。

An aggregate is one of the following types:
array type
	class type (typically, struct or union), that has
no private or protected direct (since C++17)non-static data members
	no user-declared constructors(until C++11)
no user-provided, inherited, or explicit constructors(since C++11)(until C++20)
no user-declared or inherited constructors (since C++20)
no virtual, private, or protected (since C++17) base classes
no virtual member functions
no default member initializers(since C++11)(until C++14)

The elements of an aggregate are:
for an array, the array elements in increasing subscript order, or
for a class, the non-static data members that are not anonymous bit-fields, in declaration order.(until C++17)
for a class, the direct base classes in declaration order, followed by the direct non-static data members that are neither anonymous bit-fields nor members of an anonymous union, in declaration order.

List-initialization

列表初始化感觉是针对C++初始化定义的语法,主要是为了让C++的初始化看起来更统一,所以这种初始化也被称为“uniform initialization”。
这种定义可以看到,它是在特定(常用)位置可以使用花括号来表示初始化需要的参数。这里说“特定位置”的原因在于:如果花括号位于这些位置,它们才会作为初始化内容,因为它也可能是一个语句块。

Syntax
Direct-list-initialization
T object { arg1, arg2, ... };	(1)	
T { arg1, arg2, ... }	(2)	
new T { arg1, arg2, ... }	(3)	
Class { T member { arg1, arg2, ... }; };	(4)	
Class::Class() : member { arg1, arg2, ... } {...	(5)	
Copy-list-initialization
T object = { arg1, arg2, ... };	(6)	
function ({ arg1, arg2, ... })	(7)	
return { arg1, arg2, ... };	(8)	
object [{ arg1, arg2, ... }]	(9)	
object = { arg1, arg2, ... }	(10)	
U ({ arg1, arg2, ... })	(11)	
Class { T member = { arg1, arg2, ... }; };	(12)	

以new后的括号为例

在解析new之后的初始化表达式时,对于花括号(CPP_OPEN_BRACE)号和圆括号的处理流程并不相同,流程的不同只是表面现象,这个不同跟意味着生成的语法树节点类型也不相同。
这里要注意的是,在花括号分支执行的

expression_list = make_tree_vector_single (t);

也就是虽然花括号内可能有多个元素,它是它们整体作为vector的一个元素(元素类型是init_list_type_node)。

/* Parse a new-initializer.

   new-initializer:
     ( expression-list [opt] )
     braced-init-list

   Returns a representation of the expression-list.  */

static vec<tree, va_gc> *
cp_parser_new_initializer (cp_parser* parser)
{
  vec<tree, va_gc> *expression_list;

  if (cp_lexer_next_token_is (parser->lexer, CPP_OPEN_BRACE))
    {
      tree t;
      bool expr_non_constant_p;
      cp_lexer_set_source_position (parser->lexer);
      maybe_warn_cpp0x (CPP0X_INITIALIZER_LISTS);
      t = cp_parser_braced_list (parser, &expr_non_constant_p);
      CONSTRUCTOR_IS_DIRECT_INIT (t) = 1;
      expression_list = make_tree_vector_single (t);
    }
  else
    expression_list = (cp_parser_parenthesized_expression_list
		       (parser, non_attr, /*cast_p=*/false,
			/*allow_expansion_p=*/true,
			/*non_constant_p=*/NULL));

  return expression_list;
}

Parameter pack

emplace_back是通过Parameter pack来实现。对于模板的该机制来说,它只是承担了一个中间的打解包动作:在调用位置把列表打包,在用到的位置再解包。
也就是说,调用处的多个参数在使用的时候同样是展开为多个参数。

      vector<_Tp, _Alloc>::
      emplace_back(_Args&&... __args)
      {
	if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage)
	  {
	    _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
				     std::forward<_Args>(__args)...);
	    ++this->_M_impl._M_finish;
	  }
	else
	  _M_realloc_insert(end(), std::forward<_Args>(__args)...);
#if __cplusplus > 201402L
	return back();
#endif
      }

gcc错误提示的位置

从gcc的代码看,如果一个类型type_build_ctor_call返回为false,则初始化只能有一个元素(就是前面提到的init_list_type_node)类型,而这个类型只会在特定位置被识别为这种类型。

/* Like build_x_compound_expr_from_list, but using a VEC.  */
tree
build_x_compound_expr_from_vec (vec<tree, va_gc> *vec, const char *msg,
				tsubst_flags_t complain)
{
  if (vec_safe_is_empty (vec))
    return NULL_TREE;
  else if (vec->length () == 1)
    return (*vec)[0];
  else
    {
      tree expr;
      unsigned int ix;
      tree t;

      if (msg != NULL)
	{
	  if (complain & tf_error)
	    permerror (input_location,
		       "%s expression list treated as compound expression",
		       msg);
	  else
	    return error_mark_node;
	}

      expr = (*vec)[0];
      for (ix = 1; vec->iterate (ix, &t); ++ix)
	expr = build_x_compound_expr (EXPR_LOCATION (t), expr,
				      t, complain);

      return expr;
    }
}

/* Nonzero if we need to build up a constructor call when initializing an
   object of this class, either because it has a user-declared constructor
   or because it doesn't have a default constructor (so we need to give an
   error if no initializer is provided).  Use TYPE_NEEDS_CONSTRUCTING when
   what you care about is whether or not an object can be produced by a
   constructor (e.g. so we don't set TREE_READONLY on const variables of
   such type); use this function when what you care about is whether or not
   to try to call a constructor to create an object.  The latter case is
   the former plus some cases of constructors that cannot be called.  */

bool
type_build_ctor_call (tree t)
{
  tree inner;
  if (TYPE_NEEDS_CONSTRUCTING (t))
    return true;
  inner = strip_array_types (t);
  if (!CLASS_TYPE_P (inner) || ANON_AGGR_TYPE_P (inner))
    return false;
  if (!TYPE_HAS_DEFAULT_CONSTRUCTOR (inner))
    return true;
  if (cxx_dialect < cxx11)
    return false;
  /* A user-declared constructor might be private, and a constructor might
     be trivial but deleted.  */
  for (tree fns = lookup_fnfields_slot (inner, complete_ctor_identifier);
       fns; fns = OVL_NEXT (fns))
    {
      tree fn = OVL_CURRENT (fns);
      if (!DECL_ARTIFICIAL (fn)
	  || DECL_DELETED_FN (fn))
	return true;
    }
  return false;
}

字面list initialization的模板推导

在模板类型推导过程中,init_list_type_node类型节点和unknown_type_node一样,都无法提供任何信息,也就是init_list_type_node本身没有类型信息,它需要依附/依赖于特定类型进行解析。

  if (arg == error_mark_node)
    return unify_invalid (explain_p);
  if (arg == unknown_type_node
      || arg == init_list_type_node)
    /* We can't deduce anything from this, but we might get all the
       template args from other function args.  */
    return unify_success (explain_p);
///...
}

从下面的例子中可以看到,字面的花括号在编译器内部是init_list_type_node类型节点,它没有语言用户可见的类型,例如int、float、struct等信息。

tsecer@harry$ cat init_list_type_node.cpp 
#include <vector>
#include <stdio.h>

struct tsecer
{
    int x;
    int y;
    void show()
    {
        printf("x %d y %d\n", x, y);
    }
};

int main()
{
    new tsecer{1,2};
    (new tsecer({1,2}))->show();
    std::vector<tsecer> va;
    va.emplace_back(({1, 2;}));
}

tsecer@harry$ gcc -c init_list_type_node.cpp 
In file included from /usr/include/c++/7/x86_64-redhat-linux/bits/c++allocator.h:33:0,
                 from /usr/include/c++/7/bits/allocator.h:46,
                 from /usr/include/c++/7/vector:61,
                 from init_list_type_node.cpp:1:
/usr/include/c++/7/ext/new_allocator.h: In instantiation of 'void __gnu_cxx::new_allocator<_Tp>::construct(_Up*, _Args&& ...) [with _Up = tsecer; _Args = {int}; _Tp = tsecer]':
/usr/include/c++/7/bits/alloc_traits.h:475:4:   required from 'static void std::allocator_traits<std::allocator<_Tp1> >::construct(std::allocator_traits<std::allocator<_Tp1> >::allocator_type&, _Up*, _Args&& ...) [with _Up = tsecer; _Args = {int}; _Tp = tsecer; std::allocator_traits<std::allocator<_Tp1> >::allocator_type = std::allocator<tsecer>]'
/usr/include/c++/7/bits/vector.tcc:100:30:   required from 'void std::vector<_Tp, _Alloc>::emplace_back(_Args&& ...) [with _Args = {int}; _Tp = tsecer; _Alloc = std::allocator<tsecer>]'
init_list_type_node.cpp:19:30:   required from here
/usr/include/c++/7/ext/new_allocator.h:136:4: error: no matching function for call to 'tsecer::tsecer(int)'
  { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
init_list_type_node.cpp:4:8: note: candidate: tsecer::tsecer()
 struct tsecer
        ^~~~~~
init_list_type_node.cpp:4:8: note:   candidate expects 0 arguments, 1 provided
init_list_type_node.cpp:4:8: note: candidate: constexpr tsecer::tsecer(const tsecer&)
init_list_type_node.cpp:4:8: note:   no known conversion for argument 1 from 'int' to 'const tsecer&'
init_list_type_node.cpp:4:8: note: candidate: constexpr tsecer::tsecer(tsecer&&)
init_list_type_node.cpp:4:8: note:   no known conversion for argument 1 from 'int' to 'tsecer&&'
tsecer@harry$ 

结论

aggregate initialization作为对于简单类型的直接初始化使用比较方便,但是花括号必须和目标类型有明确的关联关系。例如,在new T后面,return 之后或者函数参数位置,因为这些位置它们对应的目标类型是可以简单的推导出来。这也是C++语言“强类型”的特点。
emplace_back类是依赖模板的parameter pack,该机制本质上是对参数列表的打解包:也就是在调用处和使用处一样,都是参数列表。
字面花括号作为编译器内部使用的init_list_type_node节点,并不是C++语言使用者可以作为推导的类型。
从网上资料看,C++20有可能会允许前面编译错误的代码,也就是可以允许va.emplace_back(1, 2);来初始化数组元素。

posted on 2022-11-04 20:15  tsecer  阅读(751)  评论(0编辑  收藏  举报

导航