Skip to Content
Course content

202: Sequence Points and Undefined Behavior

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're at a busy deli. You tell the counter person, "I want a turkey sandwich and a bag of chips." In a normal world, the employee hears the whole order, processes the payment, and then hands you the food. The "payment" is the clear boundary—once that's done, the order is finalized. Now, imagine a chaotic deli where the employee starts bagging the chips while simultaneously trying to charge you for the sandwich, but they're using the same single notepad to track your total. They scribble a number, then erase it, then add to it, all at the same time. If they don't have a clear "stop and check" moment, your final bill could be anything from $0 to $100, depending on who moved their pen faster.

In C, a sequence point is that "stop and check" moment. It's a point in the execution of the program where all previous "side effects" (like changing the value of a variable) are guaranteed to be complete before the program moves on. When you write code that tries to change the same variable twice between two sequence points, you're essentially inviting that chaotic deli employee into your CPU. You've entered the realm of Undefined Behavior (UB).

The Danger of the Unspecified Order

I've seen a lot of beginners try to be "clever" by condensing their code. They think they're saving space, but they're actually writing a lottery ticket. Take a look at this:

int i = 5;
int result = i++ + ++i; 

You might think the answer is 12. Maybe you think it's 13. Here is the reality: the C standard doesn't actually say what happens here. Because there is no sequence point between the first i++ and the ++i, the compiler is free to evaluate them in any order it wants. Some compilers will give you 12, some 13, and some might do something completely bizarre because they optimized the register usage in a way you didn't expect. I always tell my juniors: if you can't tell exactly what the value is without knowing which version of GCC or Clang you're using, the code is broken.

Where the Compiler Stops Caring

Undefined Behavior is the scariest part of C because the compiler doesn't have to warn you. It doesn't throw a runtime error; it just... happens. When you trigger UB, you're telling the compiler, "I don't care what you do with this." The compiler takes that literally. It might assume that since UB is "impossible" in a correct program, it can optimize away entire chunks of your if statements or rearrange your logic in ways that seem nonsensical.

The most common sequence points you'll encounter are the semicolon (;) at the end of a statement and the logical operators && and ||. For example, in if (a == 1 && b == 2), C guarantees that a == 1 is fully evaluated before it even looks at b. That's why we can use the left side of an && to protect the right side from crashing (like checking if a pointer is NULL before dereferencing it).

The Classic Increment Trap

The most frequent "gotcha" happens inside function calls, specifically printf. You might be tempted to do something like this:

int count = 0;
printf("Current: %d, Next: %d\n", count++, ++count);

You'd expect it to print "0, 2" or "1, 1". But the C standard explicitly states that the order in which function arguments are evaluated is unspecified. The compiler can evaluate the arguments right-to-left, left-to-right, or in any order it finds convenient for the stack. I've seen this exact line of code produce different results on an ARM processor versus an x86 processor. It's a nightmare to debug because the bug disappears the moment you turn on certain compiler optimizations.

My rule of thumb? Never modify a variable and use it again in the same expression unless there is a guaranteed sequence point (like a && or a semicolon) between them. Be explicit. Write three lines of boring code instead of one line of "magic" code. Your future self—and your teammates—will thank you.




📋 Practical Task

Stabilizing the Buffer Index Counter

You have been handed a snippet of legacy code designed to track a circular buffer. The previous developer tried to be concise, but the code is exhibiting erratic behavior across different compilers because of sequence point violations.

The Broken Code:

int buffer_index = 0;
int max_size = 10;

// This line is causing Undefined Behavior
buffer_index = (buffer_index + 1) % max_size; 
// The dev actually wrote it like this in the original source:
// buffer_index = ++buffer_index % max_size; 


Actually, look at this specific problematic function:

void update_indices(int *idx, int limit) {
    // BUG: Modifying and using idx in the same expression without a sequence point
    *idx = (*idx++ % limit); 
}


Your Task:

  • Rewrite the update_indices function to achieve the same goal (incrementing the index and wrapping it around the limit) without triggering Undefined Behavior.
  • Ensure that the increment and the modulo operation happen in a clearly defined order.
  • Write a small main function to test that the index correctly wraps from limit-1 back to 0.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.