布尔变量的递减在C中是否定义?

huangapple go评论58阅读模式
英文:

Is decrement of bool variable defined in С?

问题

这个问题涉及到C语言。假设我们有如下代码:

bool a = false;
a++;
printf("%d\n", a);
a--;
printf("%d\n", a);

在我的x86-64 Linux机器上运行结果如下:

1
0

这对我来说并不令人惊讶。而这段代码:

bool a = false;
a++; a++;
printf("%d\n", a);
a--; a--;
printf("%d\n", a);

却有点让人吃惊,因为它打印出:

1
1

这在某些其他架构上也是一致的(我检查了x86和arm7)。

C标准规定e++或e--应被视为e+=1或e-=1。实际上,如果我们将a++;替换为a += 1;以及a--;替换为a -= 1;,输出结果将保持不变。

我查看了x86-64的汇编代码。GCC使用'xor'指令来进行递减:

b--; b--;
11e6:       80 75 ff 01             xor    BYTE PTR [rbp-0x1],0x1
11ea:       80 75 ff 01             xor    BYTE PTR [rbp-0x1],0x1
printf("%d\n", b);
11ee:       0f b6 45 ff             movzx  eax,BYTE PTR [rbp-0x1]
11f2:       89 c6                   mov    esi,eax
11f4:       48 8d 05 19 0e 00 00    lea    rax,[rip+0xe19]        # 2014 <_IO_stdin_used+0x14>
11fb:       48 89 c7                mov    rdi,rax
11fe:       b8 00 00 00 00          mov    eax,0x0
1203:       e8 68 fe ff ff          call   1070 <printf@plt>

而Clang则更喜欢使用'add'(加法)和'and'(与运算)来进行递减:

11c9:       04 01                   add    al,0x1
11cb:       24 01                   and    al,0x1
11cd:       88 45 fb                mov    BYTE PTR [rbp-0x5],al
11d0:       8a 45 fb                mov    al,BYTE PTR [rbp-0x5]
11d3:       24 01                   and    al,0x1
11d5:       0f b6 f0                movzx  esi,al
11d8:       48 8d 3d 36 0e 00 00    lea    rdi,[rip+0xe36]        # 2015 <_IO_stdin_used+0x15>
11df:       b0 00                   mov    al,0x0
11e1:       e8 4a fe ff ff          call   1030 <printf@plt>

但结果是一样的。如果我理解正确,这些只是翻转最低有效位的不同方法。

我所知道的教材中都没有展示这样的例子,所以我认为这不是广泛知晓的事实。可能是我的无知,但我已经用C编程了一段时间,直到现在才了解到这种奇怪(或者不奇怪?)的行为。

我的问题:

  1. 根据C标准,bool变量的递减是否定义良好?或者是未定义行为(或者可能是实现定义的行为)?

  2. 如果增加和减少bool类型是被定义的,那么为什么GCC在使用-Wall标志时会显示关于a++和a--的警告?

  3. 连续减少bool变量会将其值从0翻转为1,然后再翻转回0,依此类推。这与增加的行为不同(增加不会翻转)。这是否是故意选择的可移植行为?

英文:

This question is about С. Say we have a code like this:

bool a = false;
a++;
printf(&quot;%d\n&quot;, a);
a--;
printf(&quot;%d\n&quot;, a);

This on my x86-64 linux machine shows:

1
0

That was not a surprise for me. And this code:

bool a = false;
a++; a++;
printf(&quot;%d\n&quot;, a);
a--; a--;
printf(&quot;%d\n&quot;, a);

was kind of a surprise since it prints:

1
1

This is consistent on some other architectures (I checked x86 and arm7).

C standard says that e++ or e-- should be viewed as e+=1 or e-=1 respectively. And indeed if we replace a++; with a += 1; and a--; with a-= 1; output stays the same.

I looked at the assembly for x86-64. gcc uses 'xor' instruction to do decrement:

    b--; b--;
    11e6:       80 75 ff 01             xor    BYTE PTR [rbp-0x1],0x1
    11ea:       80 75 ff 01             xor    BYTE PTR [rbp-0x1],0x1
    printf(&quot;%d\n&quot;, b);
    11ee:       0f b6 45 ff             movzx  eax,BYTE PTR [rbp-0x1]
    11f2:       89 c6                   mov    esi,eax
    11f4:       48 8d 05 19 0e 00 00    lea    rax,[rip+0xe19]        # 2014 &lt;_IO_stdin_used+0x14&gt;
    11fb:       48 89 c7                mov    rdi,rax
    11fe:       b8 00 00 00 00          mov    eax,0x0
    1203:       e8 68 fe ff ff          call   1070 &lt;printf@plt&gt;

And clang prefers to use 'add' (!) and 'and' for decrement:

    11c9:       04 01                   add    al,0x1
    11cb:       24 01                   and    al,0x1
    11cd:       88 45 fb                mov    BYTE PTR [rbp-0x5],al
    11d0:       8a 45 fb                mov    al,BYTE PTR [rbp-0x5]
    11d3:       24 01                   and    al,0x1
    11d5:       0f b6 f0                movzx  esi,al
    11d8:       48 8d 3d 36 0e 00 00    lea    rdi,[rip+0xe36]        # 2015 &lt;_IO_stdin_used+0x15&gt;
    11df:       b0 00                   mov    al,0x0
    11e1:       e8 4a fe ff ff          call   1030 &lt;printf@plt&gt;

But the result is the same. If I understand correctly these are just different methods to flip least significant bit.

None of the textbooks that I know of show example like this, so I presume this isn't widely known fact. And probably it is my own ignorance, but I have programmed in C for some time and only now learned of this weird [or not?] behaviour.

Full source is here.

My questions:

  1. Is decrement of bool variable defined according to C standard? Or is it undefined (or maybe implementation-defined) behaviour?

  2. If increment and decrement of bools are defined, why gcc shows warnings about a++ and a-- when given -Wall flag?

  3. Сonsecutive decrements of bool variable flip its value from 0 to 1 and again to 0 and so forth. This is in contrast to what increment does (it does not flip). Is it deliberately chosen and portable behaviour?

答案1

得分: 5

以下是翻译好的部分:

  • 它被定义了(因此可移植<sup>1</sup>)。

  • C17 §6.5.2.4 ¶2 [...] 作为副作用,操作数对象的值被增加了(也就是,适当类型的值1被添加到它上面)。[...]

  • C17 §6.5.2.4 ¶23 后缀--运算符类似于后缀++运算符,只是操作数的值被减小了(也就是,适当类型的值1被从中减去)。

  • C17 §6.5.3.1 ¶2 [...] 表达式++E等价于(E+=1)。[...]

  • C17 §6.5.3.1 ¶3 前缀--运算符类似于前缀++运算符,只是操作数的值被减小了。

  • C17 §6.5.16.2 ¶3 形式为E1 op= E2的复合赋值等同于简单的赋值表达式E1 = E1 op (E2),只是左值E1只被评估一次[...]

  • (我可以继续展示加法执行整数提升,true作为int1false作为int0,等等。但你明白了。)

  • 这就是我们观察到的行为。

  • "⇒"表示整数提升或隐式转换为bool

  • 它警告因为它很奇怪。加法和减法不是布尔运算。而且有更清晰的替代方案(至少对于前缀增量和后缀增量,或者如果你丢弃了返回的值)。根据上面的内容,我们可以得出以下一些bool对象b的等价性:

    • ++b等同于b = true
    • --b等同于b = !b
  • 它是可移植的,只要你有一个C编译器。@Weather Vane表明在MSVC中,--b无条件地产生false,但众所周知,MSVC实际上并不是一个C编译器。

英文:

It is defined (and thus portable<sup>1</sup>).

> C17 §6.5.2.4 ¶2 [...] As a side effect, the value of the operand object is incremented (that is, the value 1 of the appropriate type is added to it). [...]

> C17 §6.5.2.4 ¶23 The postfix-- operator is analogous to the postfix++ operator, except that the value of the operand is decremented (that is, the value 1 of the appropriate type is subtracted from it).

> C17 §6.5.3.1 ¶2 [...] The expression++E is equivalent to (E+=1). [...]

> C17 §6.5.3.1 ¶3 The prefix-- operator is analogous to the prefix++ operator, except that the value of the operand is
decremented.

> C17 §6.5.16.2 ¶3 A compound assignment of the form E1 op= E2 is equivalent to the simple assignment expression E1 = E1 op (E2), except that the lvalue E1 is evaluated only once [...]

(I could go on to show that addition performs integer promotions, that true as an int is 1, that false as an int is 0, etc. But you get the idea.)

And that is the behaviour we observe.

bool a = false;
a++;                    # false0, 0+1=1,  1true
printf(&quot;%d\n&quot;, a);      #                            true1
a++;                    # true1,  1+1=2,  2true
printf(&quot;%d\n&quot;, a);      #                            true1
a--;                    # true1,  1-1=0,  0false
printf(&quot;%d\n&quot;, a);      #                            false0
a--;                    # false0, 0-1=-1, -1true
printf(&quot;%d\n&quot;, a);      #                            true1

"⇒" indicate integer promotion or implicit conversion to bool.

It warns because it's weird. Addition and subtraction aren't boolean operations. And there are far clearer alternatives (at least for prefix-increment and postfix-increment, or if you discard the value returned). From the above, we derive the following equivalencies for some bool object b:

  • ++b is equivalent to b = true.
  • --b is equivalent to b = !b.

  1. It's portable as long as you have a C compiler. @Weather Vane has indicated that --b produces false unconditionally in MSVC, but it's well known that MSVC is not actually a C compiler.

答案2

得分: 5

  1. 布尔变量的递减是否根据C标准定义?或者它是否是未定义的(或者可能是实现定义的)行为?

这是有定义的。

为了回答这个问题,bool 是类型 _Bool。(尽管在 <stdbool.h> 中有一个宏定义,但程序可以用不同的方式定义它。)

a++a-- 在C 2018的6.5.2.4中有规定,其中第2段说:

…作为副作用,操作数对象的值被递增(即适当类型的值1被加到它上面)。有关约束、类型、转换和操作对指针的影响的信息,请参阅加法运算符和复合赋值的讨论…

第3段说后缀 -- 类似于后缀 ++。注意有关转换的信息,请参考加法运算符。加法运算符在6.5.6中有规定,其中第4段说:

如果两个操作数都具有算术类型,则对它们执行通常的算术转换。

因此,我们有一个 bool a 和一个值1的“适当类型”。虽然“适当类型”没有正式定义,但我们可以假设它是 boolint,不管怎样结果都一样。通常的算术转换对许多读者来说是熟悉的,但它们在6.3.1.8中有规定,主要在第1段。整数操作数的第一个规则是:

…两个操作数都执行整数提升…

整数提升在6.3.1.1中有规定,第2段告诉我们,boolint 操作数被转换为 int。然后通常的算术转换规则继续:

…如果两个操作数具有相同的类型,则不需要进一步的转换。

因此,将 a 转换为 int 和1转换为 int 后,转换就停止了,a++ 的递增计算为 a 转换为 int 加1转换为 int,因此根据 a 是否从0或1开始,这将产生1或2。

然后,就像6.5.2.4中所述,我们来看复合赋值的讨论。这里的含义是 a++; 等同于 a += 1;。C 2018的6.5.16.2中的第3段说这等同于 a = a + 1;。我们已经计算出了 a + 1,所以赋值给 a 保持不变。这在6.5.16.1中有规定,其中第2段说:

…右操作数的值转换为赋值表达式的类型,并替换左操作数指定的对象中存储的值。

因此,加法的结果,1或2,被转换为 bool 并存储在 a 中。6.3.1.2告诉我们有关转换为 bool 的内容:

当将任何标量值转换为 _Bool 时,如果该值等于0,则结果为0;否则,结果为1。

因此,将1或2转换为 bool 得到1。因此,a++; 是完全定义的,并将1存储在 a 中。

  1. 如果布尔值的递增和递减是定义的,为什么gcc在使用 -Wall 标志时会显示关于 a++a-- 的警告?

C标准允许实现发出额外的诊断,一个常见的诊断类别是完全由C标准定义的但通常程序员很少使用的代码,因此其使用可能表示拼写错误或其他错误。由于 a++ 总是将 bool 设置为1,a = 1 将更清晰和常见,因此 a++ 有可能是一个错误而不是有意的代码,所以它应该得到诊断,特别是如果请求了 -Wall

同样,a--; 是不常见的。如果意图是翻转一个 bool,则 a = !a; 更常见和更熟悉。

  1. 布尔变量的连续递减会将其值从0翻转到1,然后再翻转回0,以此类推。这与递增的行为相反(它不会翻转)。这是否是故意选择的并且是可移植的行为?

这是故意的,从C的规则在多个十年内一直被委员会反复仔细考虑,并且这个行为是根据上面讨论的规则产生的,需要注意:

a--bool 转换为 int。然后我们从 bool 的0或1开始减1,得到一个 int 值-1或0。然后将该 int 转换为 bool,得到1或0,并将其存储在 a 中。

由于这是完全指定的并且是严格遵守C标准的代码,因此它在符合C标准的编译器上是可移植的。(我不断言任何微软产品与C标准兼容。)

然而,我怀疑规则的设计意图并不是为了使 a--; 翻转一个 bool 值。这更可能是规则整体设计的结果。

英文:

> 1. Is decrement of bool variable defined according to C standard? Or is it undefined (or maybe implementation-defined) behaviour?

It is defined.

For the purposes of this answer, bool is the type _Bool. (It is defined thusly in a macro in &lt;stdbool.h&gt;, but a program could define it differently.)

a++ and a-- are specified in C 2018 6.5.2.4, where paragraph 2 says:

> … As a side effect, the value of the operand object is incremented (that is, the value 1 of the appropriate type is added to it). See the discussions of additive operators and compound assignment for information on constraints, types, and conversions and the effects of operations on pointers…

and paragraph 3 says postfix -- is analogous to postfix ++. Note the reference to the additive operators for information on conversions. The additive operators are specified in 6.5.6, where paragraph 4 says:

> If both operands have arithmetic type, the usual arithmetic conversions are performed on them.

So we have a bool a and a value 1 of “the appropriate type.” “Appropriate type” is not formally defined, but we can suppose it is bool or int, and the result will be the same either way. The usual arithmetic conversions are familiar to many readers, but they are specified in 6.3.1.8, mostly in paragraph 1. The usual arithmetic conversions start with considerations of floating-point types that do not apply here. The first rule for integer operands is:

> … the integer promotions are performed on both operands…

The integer promotions are specified in 6.3.1.1, and paragraph 2 tells us that a bool or int operand is converted to int. Then the usual arithmetic conversion rules continue:

> … If both operands have the same type, then no further conversion is needed.

So, having converted a to int and 1 to int, the conversions stop, and the increment for a++ is calculated as a converted to int plus 1 converted to int, so this yields 1 or 2 according to whether a starts at 0 or 1.

Then, as 6.5.2.4 2 above says, we look to the discussion of compound assignment. The implication here is that a++; is equivalent to a += 1;. C 2018 6.5.16.2 3 says this is equivalent to a = a + 1;. We have already figured out a + 1, so the assignment to a remains. This is specified in 6.5.16.1, where paragraph 2 says:

> … the value of the right operand is converted to the type of the assignment expression and replaces the value stored in the object designated by the left operand.

Thus the result of the addition, 1 or 2, is converted to bool and stored in a. 6.3.1.2 tells us about conversions to bool:

> When any scalar value is converted to _Bool, the result is 0 if the value compares equal to 0; otherwise, the result is 1.

Thus, converting 1 or 2 to bool yields 1. Therefore, a++; is fully defined and stores 1 in a.

> 2. If increment and decrement of bools are defined, why gcc shows warnings about a++ and a-- when given -Wall flag?

The C standard allows implementations to issue extra diagnostics, and a common category of diagnostic is code that is fully defined by the C standard but that is rarely used normally by programmers, so its use may indicate a typo or other error. Since a++ always sets a bool to 1, a = 1 would be clearer and more common code, so a++ is somewhat likely to be a mistake instead of intentional code, so it deserves a diagnostic, especially if -Wall is requested.

Similarly, a--; is unusual. If the intention is to flip a bool, a = !a; is more common and more familiar.

> 3. Сonsecutive decrements of bool variable flip its value from 0 to 1 and again to 0 and so forth. This is in contrast to what increment does (it does not flip). Is it deliberately chosen and portable behaviour?

It is deliberate, in the sense the rules of C have been repeatedly and carefully considered by committees over multiple decades, and the behavior arises out of the rules discussed above, noting that:

a-- converts the bool to an int. Then we start with 0 or 1 for the bool and subtract 1, yielding an int value −1 or 0. Then that int is converted to bool, yielding 1 or 0, and that is stored in a.

Since this is fully specified and is strictly conforming C code, it is portable across compilers conforming to the C standard. (I do not assert any Microsoft product is compatible with the C standard.)

However, I doubt the rules were designed with the intent of causing a--; to flip a bool value. This is more likely a consequence of the overall design of the rules.

答案3

得分: 4

  1. 按照C标准,布尔变量的递减是定义的。bool是无符号整数类型之一,它们是整数类型,是实数类型,是算术类型。后缀递减操作符的唯一约束是:

    后缀递增或递减操作符的操作数必须具有原子、限定、或未限定的实数或指针类型,并且必须是可修改的左值。

    (C23 6.5.2.4/1)

    任何指定可修改的bool的表达式都满足这一条件,并且伴随的语义描述了结果效果(但请参见下文)。对于bool没有特殊的例外情况。

  2. 如果布尔值的递增和递减是定义的,为什么gcc在使用-Wall标志时会显示关于a++和a--的警告?

    因为对布尔值执行算术操作并没有太多意义,而且这种表达式可能不会产生您期望的结果。虽然bool被分类为无符号整数类型,但其行为与所有其他整数类型不同。

  3. 连续递减布尔变量会将其值从0翻转为1,然后再次翻转为0,依此类推。这与递增的行为不同(它不会翻转)。这是故意选择的可移植行为吗?

    这种行为与bool的行为规范和C算术规范一致。后缀递增和递减操作符的规范定义了对操作数的存储值的影响,分别为将1添加到它或从中减去1,然后将进一步的细节委托给加法操作符和复合赋值的规范。我看到的最合理的解释是:

    • a++a 的存储值的副作用与表达式 a = a + 1 的副作用相同,只是在评估 a 本身时仅评估一次。

    • a--a 的存储值的副作用与表达式 a = a - 1 的副作用相同,只是在评估 a 本身时仅评估一次。

    对于bool a,评估a + 1 首先将a 转换为int,然后将1添加到结果的int中。由于a 的类型为bool,我们可以确信结果可以表示为int。然后将该结果转换为bool 类型,对于这个特殊规则

    当将任何标量值转换为bool时,如果值为零(对于算术类型)[...],则结果为false;否则,结果为true

    (C23 6.3.1.2/1)

    这将会将a + 1 的任何可能结果转换为true

    对于后缀递减,将a - 1 转换为bool 将产生true,如果a 最初是false(0),通过中间的int 值-1,但如果a 最初是true(1),则将产生false

    总之,规范在这一点上可能可以更清晰,但我认为您指定的行为是明确定义的,这些特定操作的其他结果都是不正确的。然而,我不会仅仅依赖于它们,因为在布尔值上执行算术操作是令人困惑的,风格上容易引发争议。

英文:

> 1. Is decrement of bool variable defined according to C standard? Or is it undefined (or maybe implementation-defined) behaviour?

It is defined.

bool is among the unsigned integer types, which are integer types, which are real types, which are arithmetic types. The one constraint on the postfix decrement operator is that:

> The operand of the postfix increment or decrement operator shall have
> atomic, qualified, or unqualified real or pointer type, and shall be a
> modifiable lvalue.

(C23 6.5.2.4/1)

Any expression that designates a modifiable bool satisfies that, and the accompanying semantics describe the resulting effect (but see below). No exception is made for bools.

> 2. If increment and decrement of bools are defined, why gcc shows warnings about a++ and a-- when given -Wall flag?

Because it does not make much sense to perform arithmetic on boolean values, and because such expressions might not do what you expect. Although bool is classified as an unsigned integer type, its behavior is different from all other integer types.

> 3. Сonsecutive decrements of bool variable flip its value from 0 to 1 and again to 0 and so forth. This is in contrast to what increment
> does (it does not flip). Is it deliberately chosen and portable
> behaviour?

That behavior is consistent with the specifications for the behavior of bool and for C arithmetic.

The specifications for postfix increment and decrement operators define the effect on the operand's stored value as adding 1 to it or subtracting 1 from it, respectively, and they defer to the specifications for additive operators and compound assignment for further details. The most reasonable interpretation I see is that

  • the side effect of a++ on the stored value of a is the same as that of the expression a = a + 1, except inasmuch as a itself is evaluated only once.

  • the side effect of a-- on the stored value of a is the same as that of the expression a = a - 1, except inasmuch as a itself is evaluated only once.

For bool a, the evaluation of a + 1 proceeds by first converting a to int, then adding 1 to the resulting int. Since a has type bool, we can be confident that the result will be representable as an int. That result is then converted to type bool, for which there is a special rule:

> When any scalar value is converted to bool, the result is false if
> the value is a zero (for arithmetic types) [...]; otherwise, the result is true.

(C23 6.3.1.2/1)

That will have the effect of converting either possible result of a + 1 to true.

For postdecrement, on the other hand, converting a - 1 to bool yields true if a is initially false (0) via intermediate int value -1, but it yields false if a is initially true (1).

Overall, then, the spec could be clearer than it is on this point, but I do think that the behavior you specify is well defined, and no other result for these particular operations is correct. However, I would not rely on them simply because performing arithmetic on bools is confusing and stylistically fraught.

huangapple
  • 本文由 发表于 2023年7月18日 01:20:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/76706760.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定