The 2024 Wheel Reinvention Jam just concluded. See the results.

Undefined behavior in C/C++

A couple of questions regarding undefined behavior and integer math in C/C++:

  1. Is overflow/underflow defined for integer type? How do you check whether or not you're closed to overflow? I remember there were some differences between signed and unsigned.
  2. What happens when you cast between a signed and unsigned integer? Is it bit casting, to zero, or undefined? What about different integer sizes, and the number is out of bounds?
  3. What is the promotion rule?
  4. How do you cast between a pointer and a number, or bit-casting between two different types?
  5. Can I do pointer math on non-char pointers?
  6. Is type punning (pointer hack or passing different pointer type) defined?
  7. Is accessing multiple fields from a union possible?
  8. Can I produce a trap by dereference a null pointer or divide by zero?

You can be as specific as you want, and if there are any differences between C and C++ or between different compiler vendors (special flags, custom vendor's spec, etc), please let me know.

  1. For unsigned integers overflow/underflow is defined behavior. It performs operation % 2**n. For signed integers over/under-flow is undefined behavior. It depends on operation how to check for overflow. For example, to check if addition a+b will overflow for uint8_t types you can check if a>255-b. For unsigned types you can perform check after the operation, like uint8_t c = a + b; if (c < a) { overflow happened }. Alternatively you can cast to bigger type and check if result is not too large. For example, for int32_t types - if ((int64_t)a+(int64_t)b >= (1LL<<31)) { overflow happened }. Another alternative is to use compiler specific intrinsics, like __builtin_add_overflow for gcc/clang.

  2. When casting to larger type (32->64) the value is sign or zero extended. When casting to same bits or smaller type then for unsigned target type it is truncated (use lowest bits), or for signed type it is implementation defined behavior (not UB).

  3. Promotion rules are rules to which type values are converted when doing operation, like A+B when types for A and B are different (int and float or similar). Or when assigning A=B.

  4. You can convert pointers to intptr_t or uintptr_t type and back without UB. Other types are implementation defined, or UB when casting to smaller integer type than pointer needs.

  5. Yes, but with some restrictions. The important one is that resulting pointer must be in same range as allocated object to where original pointer points to. For example, if you have array of 10 chars, then adding up to +10 to pointer to beginning of array is OK, but adding +11 is UB. For comparisons if they are not from same "object" then result is unspecified (not UB).

  6. You can cast pointers to different types. But you cannot cast to type that has smaller alignment - that's UB. What is not allowed is dereferencing wrong types from what pointer was originally. There is exception in some cases - you are allowed to cast to char* and void* regardless of alignment. And for structures if beginning of it matches with original type.

  7. This is very tricky, rules are very different depending on standard or language. If I'm not mistaken in C89 it is implementation defined (not UB). In C99 it is kind of "allowed" (with special wording of "trap representation"). In C++ it is UB, except in situation where structure beginning is same between types of members.

  8. From spec point of view those operations are UB. Whether they will produce runtime trap exception or not depends on compiler & target architecture (UB allows compiler to completely remove code or change it to anything).


Edited by Mārtiņš Možeiko on

6 and 7 in recent C++ versions have to do with strict aliasing which makes this kind of thing undefined behavior except in specific circumstances.

The "blessed" way of doing typepunning is by doing a memcpy from one variable to another. memcpy is specifically specified to reinterpret both pointers as char* (which is one of the things that is always allowed to alias anything else) when doing the copy.

For signed integers over/under-flow is undefined behavior

Then how can I check for it?

When casting to larger type (32->64) the value is sign or zero extended.

What does sign or zero-extended mean?

When casting to same bits or smaller type then for unsigned target type it is truncated (use lowest bits), or for signed type it is implementation defined behavior (not UB)

What about signed types? And what does implementation-defined mean?

Points 5, 6, and 7 are hard for me to understand. Another comment also mentioned strict aliasing. Can you go a little deeper there? Also, I remember there's a way to turn off strict aliasing through compiler flags.

I assume when you said "array of 10 chars" you meant a static array char arr[10] where the compiler knows the size. What about malloc/VirtualAlloc or just using the pointer inside a function where the compiler doesn't know the array size or the allocated range?

For comparisons if they are not from same "object" then result is unspecified (not UB)

What does "not from same object' and "unspecified" mean?

But you cannot cast to type that has smaller alignment - that's UB

So I can't cast between a pointer to a float and a pointer to a double, because one has a 4-byte alignment while the other has 8.

with special wording of "trap representation"

What does it mean?

In C++ it is UB, except in situation where structure beginning is same between types of members.

It's hard for me to visualize it, can you give me an example?


Replying to mmozeiko (#29988)

Then how can I check for it?

Depends on operation. I showed example above for overflow check when adding of int32_t types - cast to int64_t and check if result is in bounds (need to check against max negative number for underflow to). It's just a bit of extra math. If you don't want to cast, then it's extra comparisons:

int add_safe(int a, int b)
{
    if (a > 0 && b > INT_MAX - a) { ERROR: overflow happens }
    else if (a < 0 && b < INT_MIN - a) { ERROR: underflow happens }
    return a + b; // now it is OK
}

What does sign or zero-extended mean?

Sign extended means taking top bit and replicating it on all the newly prepended top bits. Zero extended means setting them always to 0. So casting uint8_t -> int32_t puts 0 on top 24 bits. Casting int8_t -> int32_t puts 8th bit of int8_t in top 24 bits.

What about signed types? And what does implementation-defined mean?

For signed types it is implementation defined. Implementation defined means that behavior depends on target architecture and/or compiler. Meaning if you know what it does for your arch/compiler then you can use it and it is fine - it is allowed operation (vs UB which is never allowed). If it does something different from what you want, then you don't use it, you do the operation differently.

Points 5, 6, and 7 are hard for me to understand. Another comment also mentioned strict aliasing. Can you go a little deeper there? Also, I remember there's a way to turn off strict aliasing through compiler flags.

Are you asking about from C spec point of view? Or from practical point of view? Because in practice in any modern OS the real compilers will do things just fine - you can do arithmetic on pointers regardless where pointers come from or what's their alignment.

I assume when you said "array of 10 chars" you meant a static array char arr[10] where the compiler knows the size. What about malloc/VirtualAlloc

It applies also to malloc'ed arrays too. For VirtualAlloc it does not apply, because C standard does not know anything about such function or what is "virtual memory". In practice it will work just fine, but not from C standard point of view.

or just using the pointer inside a function where the compiler doesn't know the array size or the allocated range?

Compiler does not need to know array size or allocated range. It is up to you to guarantee that.

What does "not from same object' and "unspecified" mean?

int arr1[10];
int arr2[10];

&arr1[4] == &arr1[5] - this is comparison from "same object"
&arr1[4] == &arr2[5] - this is comparison NOT from same object

Similar rule applies to comparing address of struct members. Unspecified means that result value is not set to anything meaningful (from spec point of view).

So I can't cast between a pointer to a float and a pointer to a double, because one has a 4-byte alignment while the other has 8.

Technically yes. In practice on any modern OS you can do it just fine.

What does it mean?

Trap representation in spec language means that it is sometimes UB to access such value.

It's hard for me to visualize it, can you give me an example?

struct Base { int value; };
struct Extra1 { int value; int value2; };
struct Extra2 { Base base; int value2; };
struct Extra3 { float x; }

Both Extra2 and Extra1 starts with same types as Base structure. But not Extra3.


Edited by Mārtiņš Možeiko on
Replying to longtran2904 (#29990)

if (a > 0 && b > INT_MAX - a) { ERROR: overflow happens }

If I want to fake overflow the number here, can I do this return b - (INT_MAX - a)?

For signed types it is implementation defined. Implementation defined means that behavior depends on target architecture and/or compiler

Are you asking about from C spec point of view? Or from practical point of view?

Both. Is it UB in the C or C++ spec, and if it is then do different compiler vendors define it in any way? I'm asking this because I read somewhere that compilers (GCC, Clang, etc) have their own specs and usually do or guarantee to do things like strict aliasing, signed overflow/underflow, etc, and you can configure them through some special compiler flags. Is it true and what do these compilers specify about signed types or strict aliasing? I want to know what popular compilers do: GCC does this, Clang allows that, while MSVC guarantees these.

Unspecified means that result value is not set to anything meaningful (from spec point of view).

So what is the result of &arr1[4] == &arr2[5]? True or false?

Yes, but with some restrictions. The important one is that resulting pointer must be in same range as allocated object to where original pointer points to. For example, if you have array of 10 chars, then adding up to +10 to pointer to beginning of array is OK, but adding +11 is UB.

I'm still confused about this example, so let's make sure we're on the same page. I asked you if doing pointer arithmetics is defined, and your answer is yes but with a caveat:

The important one is that resulting pointer must be in same range as allocated object to where original pointer points to

Presumably, the same allocated range can be from the same malloc, the same "object", or from some static arrays like your example. But what happens when the compiler can't determine whether or not it comes from the same range? I can have a function that takes in 2 arbitrary pointers or operate on pointers from VirtualAlloc or some custom functions.

int* a = malloc(n);
int* b = malloc(n);
a - b // UB?

int* c = a + 3;
int* d = a + 10;
c - d // not UB?

int* e = a + 1024; // let's say n < 1024
e - c // UB?
e - a // UB?

int* x1 = VirtualAlloc(n);
int* y1 = VirtualAlloc(n);
x1 - y1 // UB?

int* x2 = MyFunction(...);
int* y2 = MyFunction(...);
x2 - y2 // UB?

void SomeFunction(int* a, int* b)
{
    a - b // UB?
}

Edited by longtran2904 on
Replying to mmozeiko (#29991)

If I want to fake overflow the number here, can I do this return b - (INT_MAX - a)?

If by "fake it" you mean return value same as in overflow in 2s complement, then I believe you can do return (a - INT_MAX) + (b - INT_MAX).

Both. Is it UB in the C or C++ spec, and if it is then do different compiler vendors define it in any way? I'm asking this because I read somewhere that compilers (GCC, Clang, etc) have their own specs and usually do or guarantee to do things like strict aliasing, signed overflow/underflow, etc, and you can configure them through some special compiler flags. Is it true and what do these compilers specify about signed types or strict aliasing? I want to know what popular compilers do: GCC does this, Clang allows that, while MSVC guarantees these.

signed overflow is UB in C and C++ spec. Many compilers exploit this UB to generate faster code.

Compilers do not have "spec" in "C spec" of way. They have behavior that is different from version to version and from vendor to vendor. And from code to code. Even a very small change in code can change what behavior code has in case of UB. It is very very hard to predict what will happen. If you want reliable code, then do not do UB unless you're really sure what compiler will produce. Thus the phrase "babysitting compiler", because for in many situations you need to examine resulting asm to see what compiler produced and whether it is what you wanted.

So what is the result of &arr1[4] == &arr2[5]? True or false?

It is unspecified. From spec point of view resulting value is meaningless, because comparing unrelated pointers like that may not be possible in target architecture. In any modern OS the comparison will be false. It will be just comparing pointer numbers as integers.

But what happens when the compiler can't determine whether or not it comes from the same range?

Compiler does not determine whether pointers do come or not from same range. They generate code assuming it always comes from the same range. Just like it always generates code that assumes signed overflow never happens. Or generate code that dereferences pointer assuming NULL never happens. But again, that is from spec point of view. In any modern OS compiler such arithmetic on pointers will operate with their integer value representing address in memory. But from spec point of view such arithmetic results are not legal when result is UB. When UB happens it is not because compiler does something special to check for that. It is allowed to generate code assuming it never happens. For example, if you write code like this:

int* ptr = ...;
int x = *ptr; // dereference pointer
if (ptr == NULL) { printf("error\n"); }

then compiler is allowed to completely remove that if statement - no printf will be present in generated code. Because compiler knows that ptr is not NULL - because dereferencing already happened, otherwise this situation would be UB and UB is not allowed to happen. That applies to all the UB situations.


Edited by Mārtiņš Možeiko on
Replying to longtran2904 (#29992)

Compilers do not have "spec" in "C spec" of way. They have behavior that is different from version to version and from vendor to vendor. And from code to code. Even a very small change in code can change what behavior code has in case of UB. It is very very hard to predict what will happen. If you want reliable code, then do not do UB unless you're really sure what compiler will produce. Thus the phrase "babysitting compiler", because for in many situations you need to examine resulting asm to see what compiler produced and whether it is what you wanted

Maybe using the term "spec" here is bad and confusing. Let's look at GCC for example. Even though signed overflow and strict aliasing are both UB, you can turn them off using -fstrict-overflow, -fwrapv, -fstrict-aliasing, etc. There're multiple other behaviors that you can control using other compiler flags. Actually, GCC has its own pages that document these optimized and codegen options. This was what I mostly asked about. To me, these things are "compiler spec" because they're specific to a compiler (GCC in this case) and don't change from version to version. But you can call it "extension", "documentation", or whatever you want. So I'm asking whether these flags are reliable (does GCC ignore and change from version to version), and do other popular compilers have something similar?

Regarding pointer arithmetic, does my code example UB or not (does each pointer come from the same range)?


Replying to mmozeiko (#29993)

Yes, those flags are reliable (aside occasional compiler bugs). Afaik whole linux kernel is compiled with -fwrapv and they rely on its behavior.

Your arithmetic code has UB, but I expect for the usual desktop OS'es (win/mac/linux) regular compilers (msvc/gcc/clang) will not miscompile them. They will compile to code you expect - just an integer arithmetic on pointer value.

a - b // yes, UB
c - d // yes, UB
e - c // yes, UB
e - a // yes, UB
x1 - y1 // unspecified, not UB
x2 - y2 // depends on what MyFunction returns
a - b (inside SomeFunction) // depends on where argument values come from

Edited by Mārtiņš Možeiko on
Replying to longtran2904 (#29994)

Yes, those flags are reliable (aside occasional compiler bugs). Afaik whole linux kernel is compiled with -fwrapv and they rely on its behavior.

Are there similar flags on MSVC or Clang?

c - d // yes, UB

Why c - d is UB? Don't they both come from a (assuming n > 10)?

When casting to larger type (32->64) the value is sign or zero extended. When casting to same bits or smaller type then for unsigned target type it is truncated (use lowest bits), or for signed type it is implementation defined behavior (not UB).

So cast from i32 to u32 is bit casting, while u32 to u8 results in the lower 8 bits. You said cast to signed type is implementation-defined but from smaller type to bigger type is sign-extended, so does casting from i8 to i16 define to be sign-extended while i16 to i8 is implementation defined?

I've also read somewhere that int8_t and uint8_t can be different from character type. Is it true and what does the spec say about it?

I forgot to ask about floating point casting. What is the defined behavior for casting between doubles and floats and between floats and integers?


Edited by longtran2904 on
Replying to mmozeiko (#29995)

Are there similar flags on MSVC or Clang?

Clang has most of gcc flags. Not all of them are implemented, you need to check for that. But the names of flags are the same. MSVC does not have such flags.

Why c - d is UB? Don't they both come from a (assuming n > 10)?

Sorry, you're right - that's not an UB. Both c and d are valid pointers, so result will be -7.

So cast from i32 to u32 is bit casting, while u32 to u8 results in the lower 8 bits. You said cast to signed type is implementation-defined but from smaller type to bigger type is sign-extended, so does casting from i8 to i16 define to be sign-extended while i16 to i8 is implementation defined?

Yes, that's correct.

I've also read somewhere that int8_t and uint8_t can be different from character type. Is it true and what does the spec say about it?

Yes, that's correct. uint8/int8 are exactly 8-bit wide types. How many bits are in "char" type is not exact. It is CHAR_BIT define from limits.h but it can be other than 8. Here's an example of such machine where C does not have 8-bit char type: https://begriffs.com/posts/2018-11-15-c-portability.html

I forgot to ask about floating point casting. What is the defined behavior for casting between doubles and floats and between floats and integers?

You should start reading spec :) Or at least cppreference: https://en.cppreference.com/w/c/language/conversion#Real_floating-integer_conversions

Basically floats/doubles to ints converts by truncating toward zero. If resulting value cannot be represented in target type then UB.

ints/floats/doubles to floats/doubles converts value to closest representable float with nearest rounding (on IEEE floats). If result is not represantable in target type, then UB.


Replying to longtran2904 (#29997)

Yes, that's correct. uint8/int8 are exactly 8-bit wide types. How many bits are in "char" type is not exact. It is CHAR_BIT define from limits.h but it can be other than 8. Here's an example of such machine where C does not have 8-bit char type: https://begriffs.com/posts/2018-11-15-c-portability.html

Interesting read. Reading it makes me remember a question I have: What is a word and how big is it? In Win32, there's a WORD type of 16 bytes. But I also see people use the "word" term in other places with different meanings and sizes.

You should start reading spec :) Or at least cppreference: https://en.cppreference.com/w/c/language/conversion#Real_floating-integer_conversions

This is very useful, thanks for the link. I have a couple of questions regarding this:

In the "Real floating point conversions" section:

This section is incomplete

Reason: check IEEE if appropriately-signed infinity is required

What is the defined behavior for infinity and beyond? Is there anything else missing from this section?

In the "Real floating-integer conversions" section:

  1. How can I truncate a float? Casting to an integer type and back doesn't work for all numbers.
  2. Does -0.0 round to zero in integer land, or is it unrepresentable? Does if (-0.0) equal to true or false?

In both sections:

although if IEEE arithmetic is supported, ...

Can I interpret this as: if the platform supports IEE then the compiler must use it? Is the same thing true for two complements?

In the "Notes" section:

On the other hand, although unsigned integer overflow in any arithmetic operator (and in integer conversion) is a well-defined operation and follows the rules of modulo arithmetic, overflowing an unsigned integer in a floating-to-integer conversion is undefined behavior: the values of real floating type that can be converted to unsigned integer are the values from the open interval (-1; Unnn_MAX+1).

unsigned int n = -1.0; // undefined behavior

Does this work unsigned int n = (int)-1.0;?


Replying to mmozeiko (#29999)

What is a word and how big is it? In Win32, there's a WORD type of 16 bytes.

In Windows WORD is 16-bit type. uint16_t basically. There it comes from DOS times where word was "int" type.

What "word" actually means is up to architecture/cpu. They can mean different things.

What is the defined behavior for infinity and beyond? Is there anything else missing from this section?

Between float and double INF converts to INF. Or too high/too low values of double convert to float INF. There are a lot more details in IEEE 754 standard about how float operations work. C standard won't list them all, they just say - it works like IEEE 754.

How can I truncate a float? Casting to an integer type and back doesn't work for all numbers.

You mean you want fractional value set to 0? You can do that with simple comparison: f = (f > 0x1p23 ? f : (float)(int)f); If float is higher that "23 significant" bits, then it already does not have fractional value. Otherwise, for values smaller than 2**23 it fits exactly into integer and you can roundtrip through int.

Alternative approach is to add "max significant" value and subtract it. f = f + 0x1p23 - 0x1p23; Without casting to int. That will also remove all fractional bits.

Be careful about negative numbers, calculations I wrote above are only for positives.

Does -0.0 round to zero in integer land, or is it unrepresentable? Does if (-0.0) equal to true or false?

-0.0 is equal to 0.0, thus it converts to 0 in integer = false.

Can I interpret this as: if the platform supports IEE then the compiler must use it? Is the same thing true for two complements?

Compiler is free to implement floats with IEEE standard, or other standard. That part says "if compiler implements IEEE, then following thing happens...." Compiler may have argument to switch to different floats. Although if you're talking about modern compilers nowadays, everybody does IEEE for floats.

Does this work unsigned int n = (int)-1.0;?

Yes, that is defined behavior and will give you unsigned int with all bits set to 1.

Btw, if you're not aware - modern compilers implement many UB checks as extra sanitizer. They do not catch all the UB, but most of obvious it does. So you can easily check yourself for things like this: https://godbolt.org/z/e7sczfrGj


Edited by Mārtiņš Možeiko on
Replying to longtran2904 (#30001)

What "word" actually means is up to architecture/cpu. They can mean different things.

Can you list a couple of meanings?

You mean you want fractional value set to 0? You can do that with simple comparison: f = (f > 0x1p23 ? f : (float)(int)f); If float is higher that "23 significant" bits, then it already does not have fractional value. Otherwise, for values smaller than 2**23 it fits exactly into integer and you can roundtrip through int.

Alternative approach is to add "max significant" value and subtract it. f = f + 0x1p23 - 0x1p23; Without casting to int. That will also remove all fractional bits.

Be careful about negative numbers, calculations I wrote above are only for positives.

The first way has a branch which I don't like. The second way seems great but wouldn't the compiler just optimize it out? Is there a branchless algorithm that works with both positive and negative numbers?

Some more questions regarding pointer arithmetic:

int* a = malloc(...)
int* b = malloc(...)
a - b // UB
(char*)a - (char*)b // not UB?

bool Func1(int* a, float* b)
{
    return a == b; // UB and compilers can optimize to false?
}

bool Func2(char* a, char* b)
{
    return a == b; // not UB and compilers must produce a check?
}

char* c = malloc(...)
c[-1] // UB?

Edited by longtran2904 on
Replying to mmozeiko (#30002)

Can you list a couple of meanings?

8-bit. 16-bit. 32-bit. It depends on computer architecture. There's really no need to understand more. Just read what it is for your architecture (16-bit for x86) and use it like that.

The first way has a branch which I don't like.

It typically is implemented with conditional mask, so no branches: https://godbolt.org/z/d4bsnTvao

The second way seems great but wouldn't the compiler just optimize it out?

No, it will not optimize out. Because a + b in floats can be equal to a. So you cannot remove addition. a+b-b means (a+b) - b. It does not mean a+(b-b) (then it can be removed). See: https://godbolt.org/z/dnGGGK5sn

Is there a branchless algorithm that works with both positive and negative numbers?

Sure. I assume you want "floor" function? Here are both ways: https://gist.github.com/mmozeiko/56db3df14ab380152d6875383d0f4afd SSE1 code implements it via repeated add & sub. SSE2 code implements it via (float)(int) cast.

For other similar functions like ceil or trunc the calculations are very similar. And you can write all of that also with SSE intrinsics, just plain C, but there's not much point, because that's how floats are implemented in x64 anyway - via SSE instructions.

In practice you would not write any of this. SSE4 has builtin floor/ceil instructions.

EDIT: here's example of "trunc" done without simd intrinsics: https://godbolt.org/z/f9c1aYnfK No branches generated in optimized code. (I have not fully tested it on all floats, you should do that to compare with expected values before using it)

int* a = malloc(...)
int* b = malloc(...)
a - b // UB
(char*)a - (char*)b // not UB? - it is UB, because a and b pointers are not from same allocation

bool Func1(int* a, float* b)
{
    return a == b; // UB and compilers can optimize to false? - yes
}

bool Func2(char* a, char* b)
{
    return a == b; // not UB and compilers must produce a check? - yes
}

char* c = malloc(...)
c[-1] // UB? yes, because c-1 pointer is outside of allocated buffer

Edited by Mārtiņš Možeiko on
Replying to longtran2904 (#30004)