4

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?

alex_why
  • 95
  • 3
  • 1
    `bool` is promoted to `int`, decremented, the result is converted to `bool` and assigned back. No surprise, any non-zero `int` will be `1` in `bool`. – Eugene Sh. Jul 17 '23 at 17:23
  • With MSVC `bool b = false` and then with `b++;` repeatedly applied, remains `true;`. It doesn't flip states when incremented. And although `sizeof b` is `1`, after 256 increments `b` does not become `false`. – Weather Vane Jul 17 '23 at 17:49
  • Regarding number 2. If the compiler finds a syntax error, or a constraint violation, in the code, it **must** issue a diagnostic. Those take the form of error messages, and you can't disable them. If the compiler finds something else, it **may** issue a diagnostic. Those take the form of warning messages, and you can disable them. IMO, you should not disable them. The correct way to toggle the a boolean is with the logical-NOT operator, e.g. `a = !a;` – user3386109 Jul 17 '23 at 17:54
  • @ikegami when I invert the whole test, `bool b = true;` goes `false` with `b--;` and never goes back to `true` – Weather Vane Jul 17 '23 at 17:55
  • @WeatherVane This may be another case where MSVC isn't fully compatible with the current C standard. – user3386109 Jul 17 '23 at 18:10
  • @user3386109 in another test I repeatedly inc/decrement and after changing once (if necessary) it stays the same, and then a single opposite instruction flips it. So evidently it isn't actually counting as an `int` might. – Weather Vane Jul 17 '23 at 18:13
  • 1
    @WeatherVane Right, but the C standard says that the boolean should be promoted to `int`, then incremented or decremented, and then converted back to boolean. If you do that, decrementing a boolean toggles the value, and incrementing a boolean forces it to true. MSCV seems to be using the rule the decrementing a bool forces it to false, and incrementing forces it to true. That's more sensible, but it's not compliant with the C standard. – user3386109 Jul 17 '23 at 18:21
  • @WeatherVane To be clear, `0+1=1 => true`, `1+1=2 => true` so incrementing is always true. `1-1=0 => false`, `0-1= -1 => true`, so decrementing toggles. – user3386109 Jul 17 '23 at 18:25
  • @user3386109 when continally decremented, `b--;` in MSVC `b` remains `false`. I haven't checked any numeric vaue it may have. – Weather Vane Jul 17 '23 at 18:30
  • 1
    yeah, then that's a bug. It's easy to see it's a bug. `bool b = false; printf( "%s\n", b-1 ? "true" : "false" );` and `bool b = false; printf( "%s\n", --b ? "true" : "false" );` should produce the same result according to the spec (aside from the side-effect of changing `b`). – ikegami Jul 17 '23 at 18:32
  • @WeatherVane The behavior required by the C standard is what I described in my previous comment. In my opinion, that's not sensible behavior. The two sensible things to do would be to saturate (which is what MSVC seems to be doing), or to toggle (as if by unsigned overflow of a 1-bit unsigned number). The C standard's result is not sensible, which is why gcc chose to issue a warning. Microsoft just chose to do something sensible. – user3386109 Jul 17 '23 at 18:33
  • @user3386109 I was expecting the behaviour to be the same in both directions (it is with MSVC), but behave as an unsigned 1-bit integer (which it does not). – Weather Vane Jul 17 '23 at 18:36
  • @WeatherVane Yes, that's a reasonable expectation. Unfortunately, the C standard is often dictated by backwards compatibility, rather than the most sensible design. For example C23 finally mandates two's complement as the only allowed integer representation, only about 30 years after that was readily apparent. – user3386109 Jul 17 '23 at 18:46
  • @user3386109 does C23 mandate ASCII for 7-bit characters yet? Or does it still pander to the 3 people who want backward comptibility to EBCDIC? Or will the whole thing go away with UTF-8 for example? – Weather Vane Jul 17 '23 at 18:49
  • @WeatherVane Yup, good example, it still panders to those 3 people (because there's lots of money involved). UTF-8 is the clear winner for unicode strings being transferred over the internet. But how programs deal with unicode internally is still an open issue, as far as I can tell. – user3386109 Jul 17 '23 at 18:54
  • @ikegami yes I get it now, thanks. Integer promotions. But they are pertinent to prevent the intermediate operands from overflowing when evaluating an expression, so I'm inclined to go with user3386109's "That's more sensible, but it's not compliant with the C standard." I guess is it legacy from the early days of `bool` which was shoe-horned into `int`. Personally, I never use `bool` in a C program. I can't see any need for it. – Weather Vane Jul 17 '23 at 19:11
  • @ikegami for `int` types yes, but the rule is designed for types smaller than `int`. – Weather Vane Jul 17 '23 at 19:13
  • @ikegami ffs I know that. I meant the present integer behaviour. I'm wondering now why all integer types (except `bool`) aren't automatically promoted to the largest 'natural' type for the processor. Eg x64 all `int` computations promoted to `int64_t`. – Weather Vane Jul 17 '23 at 19:18
  • @ikegami re "2 is considered true pretty much universally". No it is not. In Java, this `int bwo = 2; if(bwo) {...}` is a syntax error. It has to be the specific comparison `if(bwo != 0)`. – Weather Vane Jul 17 '23 at 20:29
  • @ikegami I do know that Java is a mainstream language which you somehow overlooked with "pretty much every". And I do know that in C any non-zero integer value is true. And that `2` isn't a boolean value, but thank you for your advice. – Weather Vane Jul 17 '23 at 21:34
  • @ikegami I guess you don't know that boolean variables take the value `true` and `false`, but in C a non zero integer value is *considered* to be boolean true. `2` is not a boolean value but an integer. `2` is not a boolean value but an integer. `2` is not a boolean value but an integer. – Weather Vane Jul 17 '23 at 23:02
  • @Weather Vane, Re "*in C a non zero integer value is considered to be boolean true.*", That's what I've been telling you!!! /// Re "*`2` is not a boolean value but an integer.*", No. As you just said, it *is* a boolean value as well in C. It's a true value, specifically. Why are you contradicting yourself?! In one sentence, you say C considers `2` a boolean (since it's a non-zero integer), and in the next you say it doesn't. – ikegami Jul 18 '23 at 14:09

3 Answers3

5

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.
ikegami
  • 367,544
  • 15
  • 269
  • 518
5
  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.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
4
  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.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157