C++标准对于大括弧初始化的规定及gcc的对应实现
问题
在初始化一个类的时候,在某些情况下,希望先初始化对象的一个字段,然后其它字段根据这个已经初始化的字段再初始化。简单来说,就是类似于这种初始化调用想精简一下
struct A
{
int x, y;
};
int tmp = foo();
A a{tmp, tmp + 10}
这种形式,是否可以不使用tmp变量而变成这种形式(假设foo函数有副作用,此时只能被调用一次):
struct A
{
int x, y;
};
int tmp = foo();
A a{foo(), a.x + 10}
因为对y的初始化依赖了a.x的值,所以搜了一下C++关于这个初始化顺序的规定。
c++标准
求值规则第10条说明
10) In list-initialization, every value computation and side effect of a given initializer clause is sequenced before every value computation and side effect associated with any initializer clause that follows it in the brace-enclosed comma-separated list of initializers.
在List-initialization的说明中,也包含了相呼应的内容
Every initializer clause is sequenced before any initializer clause that follows it in the braced-init-list. This is in contrast with the arguments of a function call expression, which are unsequenced (until C++17)indeterminately sequenced (since C++17).
gcc实现
gcc的一个bugfix满足C++标准的语法标准。从代码上看,这里的修改比较简单,只是保证这种初始化方法必然生效。
这里也反过来说明一个问题,对于通常的函数调用,他们其实默认都有相同的求值(evaluate)顺序(通常是自左向右),尽管函数调用的参数入栈顺序可能不一样。而且C++标准明确说明了函数参数求值的顺序并没有要求。
The order of evaluation of arguments is unspecified. All side effects of argument expression evaluations take effect before the function is entered.
///@file:gcc-11.0\gcc\cp\cp-gimplify.c
int
cp_gimplify_expr (tree *expr_p, gimple_seq *pre_p, gimple_seq *post_p)
{
///....
case CALL_EXPR:
if (fn_contains_cilk_spawn_p (cfun)
&& cilk_detect_spawn_and_unwrap (expr_p)
&& !seen_error ())
return (enum gimplify_status) gimplify_cilk_spawn (expr_p);
/* DR 1030 says that we need to evaluate the elements of an
initializer-list in forward order even when it's used as arguments to
a constructor. So if the target wants to evaluate them in reverse
order and there's more than one argument other than 'this', gimplify
them in order. */
ret = GS_OK;
if (PUSH_ARGS_REVERSED && CALL_EXPR_LIST_INIT_P (*expr_p)
&& call_expr_nargs (*expr_p) > 2)
{
int nargs = call_expr_nargs (*expr_p);
location_t loc = EXPR_LOC_OR_LOC (*expr_p, input_location);
for (int i = 1; i < nargs; ++i)
{
enum gimplify_status t
= gimplify_arg (&CALL_EXPR_ARG (*expr_p, i), pre_p, loc);
if (t == GS_ERROR)
ret = GS_ERROR;
}
}
break;
///...
}
这个顺序和386的顺序相反
///@file:gcc-7.3.1\gcc\config\i386\i386.h
/* We want the stack and args grow in opposite directions, even if
PUSH_ARGS is 0. */
#define PUSH_ARGS_REVERSED 1
///@file:gcc-7.3.1\gcc\gimplify.c
/* Gimplify the CALL_EXPR node *EXPR_P into the GIMPLE sequence PRE_P.
WANT_VALUE is true if the result of the call is desired. */
static enum gimplify_status
gimplify_call_expr (tree *expr_p, gimple_seq *pre_p, bool want_value)
{
///...
/* Gimplify the function arguments. */
if (nargs > 0)
{
for (i = (PUSH_ARGS_REVERSED ? nargs - 1 : 0);
PUSH_ARGS_REVERSED ? i >= 0 : i < nargs;
PUSH_ARGS_REVERSED ? i-- : i++)
{
enum gimplify_status t;
/* Avoid gimplifying the second argument to va_start, which needs to
be the plain PARM_DECL. */
if ((i != 1) || !builtin_va_start_p)
{
t = gimplify_arg (&CALL_EXPR_ARG (*expr_p, i), pre_p,
EXPR_LOCATION (*expr_p), ! returns_twice);
if (t == GS_ERROR)
ret = GS_ERROR;
}
}
}
///...
}
告警
gcc对于该语法特性并没有完全支持,因为序列点告警依然没有消除。bug提出者这哥们都魔怔了,甚至希望众筹付费解决这个问题:-)
FWIW, because of this issue, I no longer use g++ for my project, which saddens me. If there were a means to put money on some bugs, I'd be happy to drop say $50. I do not pretend that should suffice, but maybe other people would be happy to put money on their bugs, and maybe in end some bugs might be worth shooting at. Sort of crowd-funding. Of course it would require some infrastructure to support this, but maybe this would be worth considering?
序列点
思路
主要代码在下面函数是判断是否冲突判断的关键。整体的思路是类似于树形结构的先序遍历:也就是首先进入到叶子节点,记录这些叶子节点引用(读取)/修改(写入)关系,叶子节点记录之后返回到父节点进行merge,merge是判断对于同一个表达式的读写是否有冲突,如果有冲突的提示告警。
那么如何判断是“同一个表达式”呢?主要是通过candidate_equal_p函数来判断,而和每个表达式对应的还有一个writer字段,表示在哪里修改,这个数值也表示了这个表达式是否被修改过。operand_equal_p函数看起来其实还是挺麻烦的,这也意味着,如果在编译阶段开启了这种告警,编译的时间理论上也更长。
/* Create a new struct tlist and fill in its fields. */
static struct tlist *
new_tlist (struct tlist *next, tree t, tree writer)
{
struct tlist *l;
l = XOBNEW (&tlist_obstack, struct tlist);
l->next = next;
l->expr = t;
l->writer = writer;
return l;
}
/* Return nonzero if X and Y appear to be the same candidate (or NULL) */
static bool
candidate_equal_p (const_tree x, const_tree y)
{
return (x == y) || (x && y && operand_equal_p (x, y, 0));
}
实现
最常见的就是++、--以及赋值这种表达式,所以也是很容易识别出来的。
///@file: gcc-7.3.1\gcc\c-family\c-common.c
/* Walk the tree X, and record accesses to variables. If X is written by the
parent tree, WRITER is the parent.
We store accesses in one of the two lists: PBEFORE_SP, and PNO_SP. If this
expression or its only operand forces a sequence point, then everything up
to the sequence point is stored in PBEFORE_SP. Everything else gets stored
in PNO_SP.
Once we return, we will have emitted warnings if any subexpression before
such a sequence point could be undefined. On a higher level, however, the
sequence point may not be relevant, and we'll merge the two lists.
Example: (b++, a) + b;
The call that processes the COMPOUND_EXPR will store the increment of B
in PBEFORE_SP, and the use of A in PNO_SP. The higher-level call that
processes the PLUS_EXPR will need to merge the two lists so that
eventually, all accesses end up on the same list (and we'll warn about the
unordered subexpressions b++ and b.
A note on merging. If we modify the former example so that our expression
becomes
(b++, b) + a
care must be taken not simply to add all three expressions into the final
PNO_SP list. The function merge_tlist takes care of that by merging the
before-SP list of the COMPOUND_EXPR into its after-SP list in a special
way, so that no more than one access to B is recorded. */
static void
verify_tree (tree x, struct tlist **pbefore_sp, struct tlist **pno_sp,
tree writer)
{
struct tlist *tmp_before, *tmp_nosp, *tmp_list2, *tmp_list3;
enum tree_code code;
enum tree_code_class cl;
/* X may be NULL if it is the operand of an empty statement expression
({ }). */
if (x == NULL)
return;
restart:
code = TREE_CODE (x);
cl = TREE_CODE_CLASS (code);
if (warning_candidate_p (x))
*pno_sp = new_tlist (*pno_sp, x, writer);
switch (code)
{
case CONSTRUCTOR:
case SIZEOF_EXPR:
return;
case COMPOUND_EXPR:
case TRUTH_ANDIF_EXPR:
case TRUTH_ORIF_EXPR:
tmp_before = tmp_nosp = tmp_list2 = tmp_list3 = 0;
verify_tree (TREE_OPERAND (x, 0), &tmp_before, &tmp_nosp, NULL_TREE);
warn_for_collisions (tmp_nosp);
merge_tlist (pbefore_sp, tmp_before, 0);
merge_tlist (pbefore_sp, tmp_nosp, 0);
verify_tree (TREE_OPERAND (x, 1), &tmp_list3, &tmp_list2, NULL_TREE);
warn_for_collisions (tmp_list2);
merge_tlist (pbefore_sp, tmp_list3, 0);
merge_tlist (pno_sp, tmp_list2, 0);
return;
case COND_EXPR:
tmp_before = tmp_list2 = 0;
verify_tree (TREE_OPERAND (x, 0), &tmp_before, &tmp_list2, NULL_TREE);
warn_for_collisions (tmp_list2);
merge_tlist (pbefore_sp, tmp_before, 0);
merge_tlist (pbefore_sp, tmp_list2, 0);
tmp_list3 = tmp_nosp = 0;
verify_tree (TREE_OPERAND (x, 1), &tmp_list3, &tmp_nosp, NULL_TREE);
warn_for_collisions (tmp_nosp);
merge_tlist (pbefore_sp, tmp_list3, 0);
tmp_list3 = tmp_list2 = 0;
verify_tree (TREE_OPERAND (x, 2), &tmp_list3, &tmp_list2, NULL_TREE);
warn_for_collisions (tmp_list2);
merge_tlist (pbefore_sp, tmp_list3, 0);
/* Rather than add both tmp_nosp and tmp_list2, we have to merge the
two first, to avoid warning for (a ? b++ : b++). */
merge_tlist (&tmp_nosp, tmp_list2, 0);
add_tlist (pno_sp, tmp_nosp, NULL_TREE, 0);
return;
case PREDECREMENT_EXPR:
case PREINCREMENT_EXPR:
case POSTDECREMENT_EXPR:
case POSTINCREMENT_EXPR:
verify_tree (TREE_OPERAND (x, 0), pno_sp, pno_sp, x);
return;
case MODIFY_EXPR:
tmp_before = tmp_nosp = tmp_list3 = 0;
verify_tree (TREE_OPERAND (x, 1), &tmp_before, &tmp_nosp, NULL_TREE);
verify_tree (TREE_OPERAND (x, 0), &tmp_list3, &tmp_list3, x);
/* Expressions inside the LHS are not ordered wrt. the sequence points
in the RHS. Example:
*a = (a++, 2)
Despite the fact that the modification of "a" is in the before_sp
list (tmp_before), it conflicts with the use of "a" in the LHS.
We can handle this by adding the contents of tmp_list3
to those of tmp_before, and redoing the collision warnings for that
list. */
add_tlist (&tmp_before, tmp_list3, x, 1);
warn_for_collisions (tmp_before);
/* Exclude the LHS itself here; we first have to merge it into the
tmp_nosp list. This is done to avoid warning for "a = a"; if we
didn't exclude the LHS, we'd get it twice, once as a read and once
as a write. */
add_tlist (pno_sp, tmp_list3, x, 0);
warn_for_collisions_1 (TREE_OPERAND (x, 0), x, tmp_nosp, 1);
merge_tlist (pbefore_sp, tmp_before, 0);
if (warning_candidate_p (TREE_OPERAND (x, 0)))
merge_tlist (&tmp_nosp, new_tlist (NULL, TREE_OPERAND (x, 0), x), 0);
add_tlist (pno_sp, tmp_nosp, NULL_TREE, 1);
return;
case CALL_EXPR:
/* We need to warn about conflicts among arguments and conflicts between
args and the function address. Side effects of the function address,
however, are not ordered by the sequence point of the call. */
{
call_expr_arg_iterator iter;
tree arg;
tmp_before = tmp_nosp = 0;
verify_tree (CALL_EXPR_FN (x), &tmp_before, &tmp_nosp, NULL_TREE);
FOR_EACH_CALL_EXPR_ARG (arg, iter, x)
{
tmp_list2 = tmp_list3 = 0;
verify_tree (arg, &tmp_list2, &tmp_list3, NULL_TREE);
merge_tlist (&tmp_list3, tmp_list2, 0);
add_tlist (&tmp_before, tmp_list3, NULL_TREE, 0);
}
add_tlist (&tmp_before, tmp_nosp, NULL_TREE, 0);
warn_for_collisions (tmp_before);
add_tlist (pbefore_sp, tmp_before, NULL_TREE, 0);
return;
}
case TREE_LIST:
/* Scan all the list, e.g. indices of multi dimensional array. */
while (x)
{
tmp_before = tmp_nosp = 0;
verify_tree (TREE_VALUE (x), &tmp_before, &tmp_nosp, NULL_TREE);
merge_tlist (&tmp_nosp, tmp_before, 0);
add_tlist (pno_sp, tmp_nosp, NULL_TREE, 0);
x = TREE_CHAIN (x);
}
return;
case SAVE_EXPR:
{
struct tlist_cache *t;
for (t = save_expr_cache; t; t = t->next)
if (candidate_equal_p (t->expr, x))
break;
if (!t)
{
t = XOBNEW (&tlist_obstack, struct tlist_cache);
t->next = save_expr_cache;
t->expr = x;
save_expr_cache = t;
tmp_before = tmp_nosp = 0;
verify_tree (TREE_OPERAND (x, 0), &tmp_before, &tmp_nosp, NULL_TREE);
warn_for_collisions (tmp_nosp);
tmp_list3 = 0;
merge_tlist (&tmp_list3, tmp_nosp, 0);
t->cache_before_sp = tmp_before;
t->cache_after_sp = tmp_list3;
}
merge_tlist (pbefore_sp, t->cache_before_sp, 1);
add_tlist (pno_sp, t->cache_after_sp, NULL_TREE, 1);
return;
}
case ADDR_EXPR:
x = TREE_OPERAND (x, 0);
if (DECL_P (x))
return;
writer = 0;
goto restart;
default:
/* For other expressions, simply recurse on their operands.
Manual tail recursion for unary expressions.
Other non-expressions need not be processed. */
if (cl == tcc_unary)
{
x = TREE_OPERAND (x, 0);
writer = 0;
goto restart;
}
else if (IS_EXPR_CODE_CLASS (cl))
{
int lp;
int max = TREE_OPERAND_LENGTH (x);
for (lp = 0; lp < max; lp++)
{
tmp_before = tmp_nosp = 0;
verify_tree (TREE_OPERAND (x, lp), &tmp_before, &tmp_nosp, 0);
merge_tlist (&tmp_nosp, tmp_before, 0);
add_tlist (pno_sp, tmp_nosp, NULL_TREE, 0);
}
}
return;
}
}
/* WRITTEN is a variable, WRITER is its parent. Warn if any of the variable
references in list LIST conflict with it, excluding reads if ONLY writers
is nonzero. */
static void
warn_for_collisions_1 (tree written, tree writer, struct tlist *list,
int only_writes)
{
struct tlist *tmp;
/* Avoid duplicate warnings. */
for (tmp = warned_ids; tmp; tmp = tmp->next)
if (candidate_equal_p (tmp->expr, written))
return;
while (list)
{
if (candidate_equal_p (list->expr, written)
&& !candidate_equal_p (list->writer, writer)
&& (!only_writes || list->writer))
{
warned_ids = new_tlist (warned_ids, written, NULL_TREE);
warning_at (EXPR_LOC_OR_LOC (writer, input_location),
OPT_Wsequence_point, "operation on %qE may be undefined",
list->expr);
}
list = list->next;
}
}
验证
gcc对于提示的bug
可以看到,对于这种C++标准明确规定的确定行为,gcc还是给出了警告,尽管gcc生成的代码是符合语法规范的。
tsecer@harry: cat -n cpp.aggregate.init.order.cpp
1 struct A
2 {
3 A(int x, int y);
4 int x, y;
5 };
6 int foo()
7 {
8 A a{1, a.x + 6};
9 }
tsecer@harry: gcc -c cpp.aggregate.init.order.cpp -Wall
cpp.aggregate.init.order.cpp: 在函数‘int foo()’中:
cpp.aggregate.init.order.cpp:9:1: 警告:在有返回值的函数中未发现 return 语句 [-Wreturn-type]
}
^
cpp.aggregate.init.order.cpp:8:14: 警告:‘a.A::x’ is used uninitialized in this function [-Wuninitialized]
A a{1, a.x + 6};
~~^
赋值顺序
从汇编代码看,在386体系架构下,函数调用的参数是按照从右向左顺序求值的。
tsecer@harry: cat -n fun.arg.evaluate.c
1 int a, b, c, d;
2 int foo(int, int, int), bar();
3 int baz()
4 {
5 return foo(a + b, bar(), c * d);
6 }
tsecer@harry: gcc -S fun.arg.evaluate.c
cat tsecer@harry: cat fun.arg.evaluate.s
.file "fun.arg.evaluate.c"
.text
.comm a,4,4
.comm b,4,4
.comm c,4,4
.comm d,4,4
.globl baz
.type baz, @function
baz:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq %rbx
subq $8, %rsp
.cfi_offset 3, -24
movl c(%rip), %edx
movl d(%rip), %eax
movl %edx, %ebx
imull %eax, %ebx
movl $0, %eax
call bar
movl %eax, %ecx
movl a(%rip), %edx
movl b(%rip), %eax
addl %edx, %eax
movl %ebx, %edx
movl %ecx, %esi
movl %eax, %edi
call foo
addq $8, %rsp
popq %rbx
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size baz, .-baz
.ident "GCC: (GNU) 7.3.1 20180303 (Red Hat 7.3.1-6)"
.section .note.GNU-stack,"",@progbits
tsecer@harry:
同样的代码,使用compiler explorer测试gcc的arm编译器,可以看到它的函数参数就是从左向右求值,也就是先计算加法,然后函数调用,最后乘法。
a:
.zero 4
b:
.zero 4
c:
.zero 4
d:
.zero 4
baz():
stp x29, x30, [sp, -32]!
mov x29, sp
str x19, [sp, 16]
adrp x0, a
add x0, x0, :lo12:a
ldr w1, [x0]
adrp x0, b
add x0, x0, :lo12:b
ldr w0, [x0]
add w19, w1, w0
bl bar()
mov w3, w0
adrp x0, c
add x0, x0, :lo12:c
ldr w1, [x0]
adrp x0, d
add x0, x0, :lo12:d
ldr w0, [x0]
mul w0, w1, w0
mov w2, w0
mov w1, w3
mov w0, w19
bl foo(int, int, int)
ldr x19, [sp, 16]
ldp x29, x30, [sp], 32
ret
initialize list的初始化
下面是一个更直观的对比:同样是函数调用,当他们出现在initialize list中的时候是先调用foo函数,然后是bar;但是当它们同样作为tsecer函数调用参数的时候,他们的调用就是先bar,然后foo函数。
tsecer@harry: cat -n cpp.init.list.eval.order.cpp
1 struct A
2 {
3 A(int x, int y);
4 };
5 int foo(), bar(), tsecer(int, int);
6 int baz()
7 {
8 A a{foo(), bar()};
9 return tsecer(foo(), bar());
10 }
tsecer@harry: gcc -S cpp.init.list.eval.order.cpp
tsecer@harry: cat cpp.init.list.eval.order.s
.file "cpp.init.list.eval.order.cpp"
.text
.globl _Z3bazv
.type _Z3bazv, @function
_Z3bazv:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq %rbx
subq $24, %rsp
.cfi_offset 3, -24
call _Z3foov
movl %eax, %ebx
call _Z3barv
movl %eax, %edx
leaq -17(%rbp), %rax
movl %ebx, %esi
movq %rax, %rdi
call _ZN1AC1Eii
call _Z3barv
movl %eax, %ebx
call _Z3foov
movl %ebx, %esi
movl %eax, %edi
call _Z6tsecerii
addq $24, %rsp
popq %rbx
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size _Z3bazv, .-_Z3bazv
.ident "GCC: (GNU) 7.3.1 20180303 (Red Hat 7.3.1-6)"
.section .note.GNU-stack,"",@progbits
tsecer@harry:
结论
回到最开始遇到的问题:依靠c++关于initialization list的语言规范并不能解决最开始遇到的问题,抛开gcc实现的bug不谈,这个规定也只是规定了表达式的求值顺序,这个求值并不影响构造函数。只有当构造函数执行之后,对象的字段才会生效,所以还是老老实实的使用中间变量。