Is decrement of bool variable defined in С?

189 views Asked by At

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

bool a = false;
a++;
printf("%d\n", a);
a--;
printf("%d\n", 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("%d\n", a);
a--; a--;
printf("%d\n", 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("%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>

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 <_IO_stdin_used+0x15>
    11df:       b0 00                   mov    al,0x0
    11e1:       e8 4a fe ff ff          call   1030 <printf@plt>

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?

3

There are 3 answers

0
ikegami On BEST ANSWER

It is defined (and thus portable[1]).

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++;                    # false⇒0, 0+1=1,  1⇒true
printf("%d\n", a);      #                            true⇒1
a++;                    # true⇒1,  1+1=2,  2⇒true
printf("%d\n", a);      #                            true⇒1
a--;                    # true⇒1,  1-1=0,  0⇒false
printf("%d\n", a);      #                            false⇒0
a--;                    # false⇒0, 0-1=-1, -1⇒true
printf("%d\n", a);      #                            true⇒1

"⇒" 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.
0
John Bollinger On
  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.

  1. 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.

  1. С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.

0
Eric Postpischil On
  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 <stdbool.h>, 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.

  1. 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.

  1. С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.