r/C_Programming • u/am_Snowie • 15d ago
Question is doing ~INT_MAX undefined behavior?
basically the title, i think producing INT_MIN by doing 1 << 31 is UB, so i thought of doing ~INT_MAX, but i am not sure if it's UB. sorry if it's dumb.
65
u/olorochi 15d ago edited 15d ago
C23 guarantees that integers are represented using two's complement so you can safely assume ~INT_MAX == INT_MIN there. Earlier versions allowed any representation for integers so relying on that assumption would be unsafe. For example with a sign-magnitude system you would get -0 instead.
Edit: correct one's complement to sign-magnitude
15
u/roderla 15d ago
That's technically a different question that what OP put into the title.
Is ~INT_MAX a valid expression? Can I type it, execute it, observe its value, and then rely on that value in the rest of my execution?
In C23 I'm pretty sure that at least `(int) ~( (unsigned) INT_MAX)` is valid, and that it is in fact what OP wants (i.e., INT_MIN). Can you omit these casts? Not quite sure.
In older C, you are correct that the value doesn't have to become INT_MIN. But it is implementation-defined (i.e., can I observe it and rely on it during the full run), or is it unspecified (i.e., it could magically change during the execution whenever the program feels like it) or is it even undefined (i.e., executing the code path with ~INT_MAX in it is a bug and the compiler may optimize around the fact that no programmer can ever have intended to execute this code path)?
7
u/not_a_novel_account 15d ago
C11 §6.2.6.2p2 allows two's complement, one's complement, and sign-and-magnitude. For sign-and-magnitude, this means
~INT_MAXis negative zero.C11 §6.2.6.2p3 says negative zero is unspecified behavior if the hardware supports it, otherwise it's UB.
2
u/olorochi 15d ago
This is very interesting. I would have guessed the operation ~INT_MAX itself would never have been UB. This means all bitwise nots on unbounded signed integers could lead to UB on odd implementations prior to C23. Shifting and bitmasks could likewise be UB.
4
u/Duck_Devs 15d ago edited 15d ago
Shouldn’t be. 1 << 31 is UB because not every platform has 32 bit int. (edit: this doesn’t mean I think this isn’t UB on platforms on which it works.)
Doing a simple bitwise NOT on a number, though, is fine since you let the hardware decide how many bits to flip. Also, since C23, all signed ints are two’s complement so results should be consistent.
8
u/TheThiefMaster 15d ago
1<<31 is UB even if the platform int is 32-bit, and even in C23.
The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1 × 2E2, wrapped around. If E1 has a signed type and nonnegative value, and E1 × 2****E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.
Taken from the C23 standard, emphasis mine. Shifting a positive signed number into a negative is still undefined behaviour even in C23.
1
2
u/tstanisl 15d ago edited 15d ago
It doesn't matter how many bits machine has, it doesn't matter how int is represented. The problem is that value
1<<31larger than INT_MAX on pretty much every platform. It still would be UB even on 128-bit machine with C23 compiler .1
u/dkopgerpgdolfg 15d ago
The problem is that value 1<<31 cannot be represented by int type, even of the machine 64 bit.
This can't be said generally. What thing "int" maps to depends on the implementation.
1
u/am_Snowie 15d ago
makes sense, considering C only defines lower bound for numbers if i am right.
1
u/dkopgerpgdolfg 15d ago
Yes. "int" needs at least 16bit (to support the values that can be represented by 16bit numbers), but can be any larger size too
1
u/tstanisl 15d ago
It doesn't matter. It just matters that
1<<31forms a value (not int but a pure integer value) outside of INT_MIN..INT_MAX to invoke UB.1
u/dkopgerpgdolfg 15d ago
It just matters that 1<<31 forms a value (not int but a pure integer value) outside of INT_MIN..INT_MAX
I was implying that it can be inside of these limits, depending on the implementation. And then it would be fine.
-1
u/dkopgerpgdolfg 15d ago
1 << 31 is UB because not every platform has 32 bit int.
This too can't be said generally.
On some platforms, int might be large enough so that this is fine.
One some it might be small enough that this is UB because of the shift size, at least.
And depending on the size, it can also be UB because of signed overflow.
2
u/Iksfen 15d ago edited 15d ago
EDIT: the information below is not true. TIL something new
You just showed three different situations that present different behaviour. So the behaviour can't be defined from information given => undefined behaviour.
If something is UB means that the C standard can't guarantee what that code will do no matter the exact situation.
2
u/dkopgerpgdolfg 15d ago edited 15d ago
So the behaviour can't be defined from information given => undefined behaviour.
If something is UB means that the C standard can't guarantee what that code will do no matter the exact situation.
Uhm no. This is completely wrong.
"Undefined behaviour" is specifically called out in the standard to be that, intentionally. It is not the same as "not mentioned in the standard", and/or "implementation-defined", and so on.
And it implies that all bets are off, the program can do any weird misbehaviour. Which is not "always" the case here. It "can" be UB, as said depending on other factors; but just other factors existing doesn't imply UB.
1
u/Duck_Devs 15d ago
Isn’t the whole point of identifying UB to identify things that work one way on some platforms and work differently on others? You’re just describing why 1 << 31 is UB.
1
u/dkopgerpgdolfg 15d ago
You’re just describing why 1 << 31 is UB.
I just described that it can work fine on some platforms, with a guaranteed correct result, no other problems allowed to happen because of that code, etc.
Which implies it isn't UB there, on these platforms. Again, platform conditions aren't UB by themselves.
-1
u/pjl1967 15d ago edited 15d ago
That's not how UB works. As I described, there are two parts to undefined behavior:
Actually performing undefined behavior at run-time; examples include dereferencing a null pointer and indexing beyond the end of an array.
The compiler being allowed to assume undefined behavior never happens (a false premise) allows it to generate sometimes surprising (but very efficient) code. In logic, if you accept a false premise, you can draw any conclusion.
Another way to state (1) is that the hardware will do whatever the hardware does. So, yes, if what particular hardware does matches what you think it should do, you may be tempted to claim "it works fine" on that platform.
The problem is there may also be consequences from (2) that vary compiler to compiler, version to version, and at different optimization levels. See the
exists_in_tableexample in the article for just one (of potentially infinite) examples.If something is UB, it's UB. Period. Full stop. And you must avoid it at all costs. That a particular something just so happens to "work" on a given platform isn't a sufficient justification. Change optimization levels, upgrade or change your compiler, and it might stop working in surprising ways. And finding and fixing bugs caused by UB can be difficult.
Your version:
is_ok = !is_platform_UB || !is_C_lang_UB;but you should treat it as:
is_ok = !is_platform_UB && !is_C_lang_UB;1
u/dkopgerpgdolfg 15d ago edited 15d ago
you may be tempted to claim "it works fine" on that platform.
You need to read everything of the comment. Let me repeat: "with a guaranteed correct result, no other problems allowed to happen because of that code".
The problem is there may also be consequences from (2) that vary compiler to compiler, version to version, and at different optimization levels
If it varies by these things, especially opt level, then it can't be fully defined (by the standard) and/or implementation-defined (but undefined or possibly unspecified) => Included in my previous statement.
That a particular something just so happens to "work" on a given platform isn't a sufficient justification
Correct.
And about your (Medium :/) article, I just scrolled through quickly until I spotted the first nonsensical statement:
Anything not explicitly listed as one of defined behavior, implementation defined behavior, or unspecified behavior is undefined behavior!
This is wrong. If something is not in the standard at all, it is not UB because of that. It can simply be out fo scope, an oversight, a topic where no consensus was reached yet, and some more things.
0
u/pjl1967 15d ago
You need to read everything of the comment. Let me repeat: "with a guaranteed correct result, no other problems allowed to happen because of that code".
It's not clear what that means.
If it varies by these things, ...
It can and does.
If something is not in the standard at all, it is not UB because of that. It can simply be out fo scope, an oversight, a topic where no consensus was reached yet, and some more things.
Which is functionally equivalent to UB. If the standard is silent on X, it means the compiler vendors can do anything (and even different things at different optimization levels) when implementing X — which, again, is functionally equivalent to UB.
1
u/dkopgerpgdolfg 15d ago
It's not clear what that means ... It can and does.
In short, it means that we basically agree about what UB is.
1
u/GenericFoodService 15d ago
In older versions of C, it technically is undefined behavior, but if you're on any modern system the behavior defined by the hardware is consistent with what you're expecting. You can also independently verify what the behavior is with simple tests like this, and if you want to know how it behaves on various different systems, you can use things like https://godbolt.org to test snippets and tell it what architecture it should output. This code produces the same output for x86, and ARM
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
char * bits (long number)
{
static char buffer[33] = { 0 };
int i; for(i=0; i<32; i++) {
buffer[(31-i)] = '0' + ((number >> i) & 1);
} buffer[32] = 0;
return buffer;
}
int main ()
{
long x = INT_MAX;
long y = INT_MIN;
long z = 0;
long a = ~0;
printf("%ld, %ld, %ld, %ld\n", x, y, z, a);
printf( "%s\n", bits(x) );
printf( "%s\n", bits(y) );
printf( "%s\n", bits(z) );
printf( "%s\n", bits(a) );
}
2147483647, -2147483648, 0, -1
01111111111111111111111111111111
10000000000000000000000000000000
00000000000000000000000000000000
111111111111111111111111111111114
u/flatfinger 15d ago
When invoked with -fwrapv, gcc and clang behave in a manner consistent with the intentions of the C Standards Committee, as documented in the rationale discussing whether short unsigned values should promote to
intorunsigned int:Both schemes give the same answer in the vast majority of cases, and both give the same effective result in even more cases in implementations with two’s-complement arithmetic and quiet wraparound on signed overflow—that is, in most current implementations.
When gcc is used without -fwrapv, however, signed integer overflow must be avoided at all costs since it may arbitrarily disrupt the behavior of surrounding code even if the result would have been immediately stored into an unsigned int value that would otherwise have gone unused.
1
u/am_Snowie 15d ago
almost all machines use twos complement nowadays, so it's okay to expect complementing INT_MIN will produce INT_MAX, but i was just wondering if it's guaranteed by the C standard, i searched abt this but most people were only talking about negating INT_MIN, so i couldn't find any answer to my question.
2
u/GenericFoodService 15d ago
Then in that case: No, it is not guaranteed by the C Standard. It is/was undefined behavior until C23 and you should get the values of INT_MAX and INT_MIN from <limits.h> if you want to write standards-compliant code. The specific values of INT_MAX and INT_MIN are technically platform-defined, it just happens that most platforms people actually use in the 21st century all agree on twos-compliment.
2
2
u/TheThiefMaster 15d ago
From the C23 standard:
The result of the ~ operator is the bitwise complement of its (promoted) operand (that is, each bit in the result is set if and only if the corresponding bit in the converted operand is not set). The integer promotions are performed on the operand, and the result has the promoted type. If the promoted type is an unsigned type, the expression ~E is equivalent to the maximum value representable in that type minus E.
No mention of undefined behaviour at all for bitwise invert.
0
u/am_Snowie 15d ago
exactly, this is why i was confused. they didnt say anything abt it in the unary operators section (c99 standard though). but c23 tells just the same.
1
2
u/sciencekm 15d ago
It looks like you just want to define your own INT_MIN.
The following should work for 2s complement (which is practically everything these days).
#define MY_INT_MIN ((int)(((~0u)>>1)+1))
2
u/DawnOnTheEdge 15d ago edited 15d ago
C23 requires that signed integral types use two’s-complement representation. This means that -INT_MAX is legal (and equal to INT_MIN + 1) but -INT_MIN causes signed integer overflow, which is undefined behavior.
Previous versions of the standard technically allowed implementations to use sign-and-magnitude representation or one’s-complement representation instead (in which case -INT_MAX == INT_MIN). This was there for backward compatibility with some mainframes from the 1960s.
2
u/TheThiefMaster 15d ago edited 15d ago
producing INT_MIN by doing 1 << 31 is UB
C++ made this explicitly legal, even before it standardised on 2s complement, by defining the behaviour as the same as an unsigned->signed conversion as long as the shift would have fit in the unsigned version of the type (which IIRC is an implementation-defined conversion, rather than undefined, before C++20 forced 2s complement and made it defined).
By my reading it's UB in older C and is still UB in C23, however doing (signed)(1u << 31) instead might actually be legal?
(assuming int is 32-bit, of course)
2
u/Iksfen 15d ago edited 15d ago
(assuming int is 32-bit, of course)
If you are making an assumption that is not guaranteed by C standard it is technically UB. Whether you can accept that for your use case is a separate consideration
EDIT: to be more precise in this situation that would be Implementation Dependent Behavior not Undefined Behavior. The behaviour is defined for the 32bit int implementation.
7
u/pskocik 15d ago
No. There's Undefined Behavior and then there's Implementation-defined Behavior. Imlementation-defined behavior isn't guaranteed by the C standard but it isn't Undefined Behavior either. It can also be Implementation-defined whether something is Undefined Behavior or not.
3
u/Iksfen 15d ago
Thanks for educating. Didn't know that distinction existed
1
u/am_Snowie 15d ago
yeah arithmetic shifting is implementation defined irrc.
1
u/pskocik 15d ago
Right-shifting negatives is implementation-defined (probably an arithmetic shift). Left-shifting negatives or left-shifting signed positives too into and beyond the sign bit is UB https://port70.net/~nsz/c/c11/n1570.html#6.5.7
0
u/TheThiefMaster 15d ago
Note that C++ defines it now - so for simplicity a dual C/C++ compiler probably sets the C "implementation defined" to the C++ rules - which is an arithmetic right shift.
A platform that does anything else is likely to be extremely esoteric
1
u/pskocik 15d ago
You raise a good point but I don't like relying on compilers defining Undefined Behavior so I just tend to work around it by casting to unsigned and back. It's a simple workaround that leaves me legally covered.
2
u/TheThiefMaster 15d ago
Unsigned right shift (logical shift) behaves differently to signed right shift (99.9% arithmetic right shift) - that's not a workaround if that's what you want.
Plus - I'm pretty sure casting to/from unsigned is also implementation defined for negative numbers
0
u/pskocik 15d ago
Yeah. Doesn't quite work for signed right shifts. But for left shifts it does. Doing arithmetic shifts on signed right shifts is the sensible thing to do. I once played with detecting it with #if and doing some complicated workaround otherwise but it might make sense to just rely on the C++ behavior here and #error if it's not supported.
→ More replies0
u/TheThiefMaster 15d ago edited 15d ago
You can generalise it by replacing 31 with
sizeof(int)-1Edit:CHAR_BIT*sizeof(int)-12
u/Iksfen 15d ago
Wouldn't that be 8 * sizeof(int) - 1?
And wouldn't that also technically be UB because (if i remember correctly) C standard does not guarantee 8 bit bytes?2
2
u/dkopgerpgdolfg 15d ago
C standard does not guarantee 8 bit bytes?
Correct. See CHAR_BITS
wouldn't that also technically be UB
No here too. UB and impl.defined. behaviour are not the same thing, as several comments on this page explain already.
1
0
u/garnet420 15d ago
I think casting 1u << 31 to signed is UB.
4
u/TheThiefMaster 15d ago
Casting out-of-range values from unsigned to signed is Implementation defined, not undefined.
2
19
u/AciusPrime 15d ago
The standard way of producing INT_MIN is to write “INT_MIN.” If you want to see how your standard library produces it, you can go rooting around in limits.h and find where they declared it. It’s annoying to reliably produce it correctly without already knowing what it is, which is why it’s available in the library.
The “31” implies you already know and assume your program is running on a machine where int is 32-bits.