r/cpp 4d ago

Might have found a (tiny, nuisance) bug in g++ 16.2.1 and -Wconversion?

I am far from presuming myself a compiler expert, and no this was not AI, I promise. I'm simply a long-time user of gcc/g++, and it's not everyday I am confident enough that a bug is not in my code but with the tool, haha. I'm pretty warning-sensitive so this kind of thing catches my attention. Wondering if I'm missing something, so asking it here first.

g++ version: g++ (GCC) 16.2.1 20260819 (Red Hat 16.2.1-2) (I'm on Fedora 44)
C++20

Given lines like this:

std::uint16_t count = 0;
// int other_var set elsewhere
count += (other_var == 5); // <-- -Wconversion flags this

specifically, the warning is:

 <file>:<line>:<col>: warning: conversion from ‘int’ to ‘uint16_t’ {aka ‘short unsigned int’} may change value [-Wconversion]
    <line> |       count += (other_var == 5);

-Wconversion /w either -O0 and -O3 (my usual build optimization lvls to catch optimization-dependent warnings), this gets flagged.

Well, the result of a boolean expression is 0 or 1, so obviously, there should not be a conversion. Even with the accumulate op here and integer promotion, there shouldn't be a warning (you can do count += 1 or count++ and it definitely doesn't flag -Wconversion). And more notably, the below code doesn't get flagged:

std::uint16_t count = 0;
// int other_var set elsewhere
bool match = (other_var == 5);
count += match;

I also noted that clang with -Wconversion and the same optimization lvls does not flag this. I also know I've done this before in C with other versions of gcc and don't get flagged with this.

So, am I missing something or am I right to suspect this is a possible bug with at least my version of g++?

28 Upvotes

24 comments sorted by

24

u/TheRealSmolt 4d ago

I think the standard defined implicit bool conversion is to int specifically. That's how I read cppreference, at least. I wouldn't call this a bug, though it is an interesting regression if what you are saying about the versions is true.

17

u/Som1Lse 3d ago

No.

Let's start with x += y;, where both x and y are std::uint16_t aka unsigned short. Should that warn? Because according to the usual arithmetic conversions that is first converted to int + int, evaluated, and then converted back to std::uint16_t.

What about x += y & 1;, where y is an int. Should that warn? Because the right-hand-side is definitely an int, but GCC is smart enough to notice that y & 1 always fits inside a std::uint16_t and doesn't warn.

What about x += y;, where y is a bool. Should that warn? Because GCC doesn't. Again, even though it is converted to an int first GCC is smart notice that a bool always fits inside a std::uint16_t.

It's only once you use a condition that the warning appears like x += (y == 42);. You don't get a warning if you assign the condition to an intermediate value like auto b = (y == 42); x += b;.

Godbolt link.

And as other people have pointed out, this isn't behaviour mandated by the standard. It's a warning that's supposed to be helpful, and the warning clearly isn't helpful in this case.

10

u/comfortcube 3d ago

Love how you broke that down. I wanted to say what you were saying here but didn't have the elegance. Thank you!

4

u/TheRealSmolt 3d ago

The way I see it:

  1. Regardless of the method of operation, it's still std::uint16_t + std::uint16_t = std::uint16_t
  2. Heuristically, the compiler can understand that int & 1 won't cause narrowing issues.
  3. y is definitely a bool, so it's a safe 0 or 1.
  4. (y == 42) is implicitly an int, therefore can narrow.

You can actually change it to x = x + (y == 42) & 1 and reproduce the implicit masking narrowing.

Obviously, GCC can infer that it's safe, but clearly does not. And if I go back across different versions, GCC has always behaved this way. I definitely feel that this is something that can be improved, but I wouldn't call it a bug. I believe it arises because of standard behavior, but that doesn't mean it has to behavior this way.

2

u/jwakely libstdc++ tamer, LWG chair 2d ago

What about x += y;, where y is a bool. Should that warn? Because GCC doesn't. Again, even though it is converted to an int first GCC is smart notice that a bool always fits inside a std::uint16_t.

A bool fits in uint16 but that's not what is happening in x += y, it's not just a bool that has to fit, it's the existing value of x plus one that has to fit. That's not a bool.

Now GCC shouldn't warn for OP's case if it doesn't warn for your similar cases, but everybody keeps talking about how the right operand is just a bool, but ignoring that the left operand is added to it. (And at -O0 the compiler doesn't see that the left operand is zero ... dunno why it warns at -O3 though).

1

u/jwakely libstdc++ tamer, LWG chair 2d ago

I think what's happening is that GCC has added special exceptions to handle the uint16_t case on the basis that unsigned types don't overflow they wrap to zero. So although the value can change here, it's equivalent to ++count and it's fine if that wraps to zero. But the special exceptions that suppress the warning for the other four forms in the godbolt link don't match the count += (y == 42) case, so the warning is still given.

19

u/ezoe 4d ago

GCC manual said

Warn for implicit conversions that may alter a value. This includes conversions between real and integer, like abs (x) when x is double; conversions between signed and unsigned, like unsigned ui = -1; and conversions to smaller types, like sqrtf (M_PI). Do not warn for explicit casts like abs ((int) x) and ui = (unsigned) -1, or if the value is not changed by the conversion like in abs (2.0).

https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wconversion

Meaning, it doesn't warn ALL implicit conversions. It warns ONLY IF it "may alter a value"

Given that, bool to std::uint16_t conversion never alter a value. The values of bool is either 0 or 1. Both can be expressed without a possibility of altering a value by std::uint16_t.

5

u/TheRealSmolt 4d ago

Yeah but the problem is that it's not bool to std::uint16_t, it's bool to int to std::uint16_t. So it's seeing the possibility of narrowing a value.

14

u/ezoe 4d ago

The important point is, GCC is not a standard. GCC is an implementation of standard.

GCC don't see it as arbitrary value of int to std::uint16_t conversion. GCC can determine value can be either 0 or 1 and nothing else from the context.

2

u/comfortcube 4d ago

Yeah, but it's not doing so consistently, if that was the case. If I use bool match = (a == 5); then count += match, that's fine.

8

u/TheRealSmolt 4d ago

The implicit conversion I'm deriving from applies only to prvalues, so no idea about the lvalue version.

8

u/comfortcube 3d ago

Ah, that's a good point you mentioned it. I didn't pay attention to the lvalue vs prvalue nuance.

13

u/TheRealSmolt 3d ago

Well it's batshit insane so no worries.

1

u/comfortcube 4d ago

Given that, bool to std::uint16_t conversion never alter a value. The values of bool is either 0 or 1. Both can be expressed without a possibility of altering a value by std::uint16_t.

Yeah exactly, that's why I wonder if this is a tiny bug or not. bool to std::uint16_t should be fine ¯_(ツ)_/¯

2

u/ezoe 4d ago

The important point is, GCC is not a standard. GCC is an implementation of standard.

GCC don't see it as arbitrary value of int to std::uint16_t conversion. GCC can determine value can be either 0 or 1 because it originated from bool and nothing else from the context.

This GCC option don't warn implicit TYPE conversion. GCC warn implicit VALUE conversion.

6

u/StaticCoder 3d ago

The importanter point is that this is a warning. The standard doesn't mandate any warnings.

1

u/bert8128 3d ago

OP didn’t mention the c++ standard. They said that the warning is triggered for a case where there can be no conversion error.

2

u/comfortcube 3d ago

Ah sorry, C++20. I'll update the post.

1

u/jwakely libstdc++ tamer, LWG chair 2d ago

It's not bool to uint16. You're adding a uint16 and bool, which covers to int + int, which can be larger than UINT16_MAX, so when converted back to uint16 it doesn't fit.

If the uint16 value is originally zero then that can't happen, but at -O0 the compiler doesn't do value propagation, so it only looks at the types involved not the values.

This definitely changes the value from an int with value UINT16_MAX+1 to a uint16_t with value 0:

uint16_t count = UINT16_MAX;
count += true;

It's equivalent to:

count = (int)count + (int)true;

So it's not true that adding bool to uint16_t can't give the wrong result.

But I'm not sure why GCC warns inconsistently depending on the type involved on the right hand side of the +=, I haven't looked into the details.

6

u/sheckey 3d ago

I know it’s not your point, and your point is revealing notable conversion issues (that are new to me, so thank you) but I have never really liked these kind of logic-as-computation statements, even back in the K&R days. I always have to mentally decode them. I prefer “if (match) ++count”. You have reminded me again to watch out for this kind of thing in our ancient code base. Thanks!

5

u/comfortcube 3d ago edited 3d ago

Ah, I definitely get you.

I tend to reach for these kind of statements when I want to explicitly force avoiding branching for performant code (which is where I noticed this), cuz branch prediction penalties can drop performance.

That said, I am almost certain that many compilers know to optimize this kind of thing even if one uses the if statement, and often whenever I compare assembly output with both forms, they collapse to the same. It's just a habit of mine at this point and a slight lack of confidence in compiler optimization capabilities. I should probably use the branched version more often.

Edit: Did a quick test on Godbolt /w gcc 16.2 and yup, branch vs unbranched yields the same assembly: https://godbolt.org/z/7Mn6o3Go9

3

u/sheckey 3d ago

Whoa ok that was all new to me.  Good stuff!  Thanks you and /u/azswcowboy

5

u/azswcowboy 3d ago

Yeah modern compilers are really good at optimizing these simpler cases now. The other week I noticed that pass by value std::string was same assembly as std::string_view by value. The compiler noticed that the inline function didn’t modify the passed string and elided the copy. These days I tend to write the code I want to read and only worry about optimizing if it’s in a critical hot path.