Control Flow

Programs need to make decisions and repeat work. C and Rust share the same fundamental constructs but differ in important ways around type safety, exhaustiveness, and what counts as a boolean. This chapter covers every branching and looping construct you will use in systems programming.

if / else

C

/* if_else.c */
#include <stdio.h>

int main(void)
{
    int temp = 37;

    if (temp > 100) {
        printf("boiling\n");
    } else if (temp > 0) {
        printf("liquid\n");
    } else {
        printf("frozen\n");
    }

    return 0;
}

C's if condition is any expression. Zero is false, non-zero is true. Parentheses around the condition are required.

Rust

// if_else.rs
fn main() {
    let temp = 37;

    if temp > 100 {
        println!("boiling");
    } else if temp > 0 {
        println!("liquid");
    } else {
        println!("frozen");
    }
}

No parentheses around the condition (optional but idiomatic to omit). The condition must be of type bool. You cannot write if temp { ... } when temp is an integer.

if as an expression in Rust

// if_expression.rs
fn main() {
    let temp = 37;
    let state = if temp > 100 {
        "boiling"
    } else if temp > 0 {
        "liquid"
    } else {
        "frozen"
    };
    println!("water is {}", state);
}

Both arms must return the same type.

Rust Note: Because if is an expression, Rust has no need for a ternary operator. let x = if cond { a } else { b }; replaces C's x = cond ? a : b;.

Truthiness: 0 Is False in C

C: integers as booleans

/* truthiness.c */
#include <stdio.h>

int main(void)
{
    int x = 0;
    int y = 42;
    int *p = NULL;

    if (x) printf("x is truthy\n");
    else   printf("x is falsy\n");

    if (y) printf("y is truthy\n");
    else   printf("y is falsy\n");

    if (p) printf("p is non-null\n");
    else   printf("p is null\n");

    return 0;
}

In C: 0, 0.0, NULL, and '\0' are all false. Everything else is true.

Rust: only bool is bool

// truthiness_rust.rs
fn main() {
    let x: i32 = 0;
    // if x { } // ERROR: expected `bool`, found `i32`

    if x == 0 {
        println!("x is zero");
    }

    let p: Option<i32> = None;
    if p.is_none() {
        println!("p is None");
    }
}

Caution: In C, if (x = 0) assigns 0 to x and evaluates to false. This is a common bug that compilers warn about but do not reject. In Rust, if x = 0 is a type error because assignment returns (), not bool.

Try It: In C, write if (x = 5) (single equals) inside an if statement. Compile with -Wall. Read the warning. Then try the same in Rust.

The Ternary Operator (C Only)

/* ternary.c */
#include <stdio.h>

int main(void)
{
    int x = 7;
    const char *parity = (x % 2 == 0) ? "even" : "odd";
    printf("%d is %s\n", x, parity);

    int sign = (x > 0) ? 1 : (x < 0) ? -1 : 0;
    printf("sign of %d is %d\n", x, sign);

    return 0;
}

Rust replacement -- if/else expressions:

// ternary_rust.rs
fn main() {
    let x = 7;
    let parity = if x % 2 == 0 { "even" } else { "odd" };
    println!("{} is {}", x, parity);
}

while Loops

/* while_loop.c */
#include <stdio.h>

int main(void)
{
    int i = 0;
    while (i < 5) {
        printf("%d ", i);
        i++;
    }
    printf("\n");
    return 0;
}
// while_loop.rs
fn main() {
    let mut i = 0;
    while i < 5 {
        print!("{} ", i);
        i += 1;
    }
    println!();
}

Rust has no ++ or -- operators. Use i += 1 and i -= 1.

do-while (C Only)

/* do_while.c */
#include <stdio.h>

int main(void)
{
    int i = 10;
    do {
        printf("%d ", i);
        i++;
    } while (i < 5);  /* condition is false, but body ran once */
    printf("\n");
    return 0;
}

Rust has no do-while. The idiomatic replacement uses loop:

// do_while_rust.rs
fn main() {
    let mut i = 10;
    loop {
        print!("{} ", i);
        i += 1;
        if i >= 5 { break; }
    }
    println!();
}

for Loops

C

/* for_loop.c */
#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 5; i++) {
        printf("%d ", i);
    }
    printf("\n");

    int nums[] = {10, 20, 30, 40, 50};
    size_t len = sizeof(nums) / sizeof(nums[0]);
    for (size_t i = 0; i < len; i++) {
        printf("%d ", nums[i]);
    }
    printf("\n");

    return 0;
}

Rust

// for_loop.rs
fn main() {
    for i in 0..5 {
        print!("{} ", i);
    }
    println!();

    let nums = [10, 20, 30, 40, 50];
    for n in &nums {
        print!("{} ", n);
    }
    println!();

    for (i, n) in nums.iter().enumerate() {
        print!("[{}]={} ", i, n);
    }
    println!();
}
C for loop anatomy:
  for (init; condition; update) { body }

Rust for loop anatomy:
  for variable in iterator { body }

Try It: In Rust, change 0..5 to 0..=5 (inclusive range). What is the difference in output?

loop: Rust's Infinite Loop

// loop_demo.rs
fn main() {
    let mut count = 0;
    let result = loop {
        count += 1;
        if count == 10 {
            break count * 2;  // loop can return a value via break
        }
    };
    println!("result = {}", result);
}

In C, you write while (1) or for (;;):

/* infinite_loop.c */
#include <stdio.h>

int main(void)
{
    int count = 0;
    int result;
    for (;;) {
        count++;
        if (count == 10) {
            result = count * 2;
            break;
        }
    }
    printf("result = %d\n", result);
    return 0;
}

Driver Prep: Kernel code is full of infinite loops. The main kernel thread never returns. Device polling loops use while (1) with break on status changes. Rust's loop maps directly to this pattern.

break and continue

Both languages support break (exit the loop) and continue (skip to next iteration). The semantics are identical.

/* break_continue.c */
#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 10; i++) {
        if (i == 3) continue;
        if (i == 7) break;
        printf("%d ", i);
    }
    printf("\n");
    return 0;
}
// break_continue.rs
fn main() {
    for i in 0..10 {
        if i == 3 { continue; }
        if i == 7 { break; }
        print!("{} ", i);
    }
    println!();
}

Both print: 0 1 2 4 5 6

Loop Labels (Rust)

Rust allows labeling loops and breaking/continuing to an outer loop by name.

// loop_labels.rs
fn main() {
    'outer: for i in 0..5 {
        for j in 0..5 {
            if i + j == 6 {
                println!("breaking outer at i={}, j={}", i, j);
                break 'outer;
            }
            if j == 3 {
                continue 'outer;
            }
            print!("({},{}) ", i, j);
        }
    }
    println!("done");
}

C has no loop labels. The typical workaround is a flag variable:

/* break_outer.c */
#include <stdio.h>

int main(void)
{
    int done = 0;
    for (int i = 0; i < 5 && !done; i++) {
        for (int j = 0; j < 5; j++) {
            if (i + j == 6) {
                printf("breaking at i=%d, j=%d\n", i, j);
                done = 1;
                break;
            }
        }
    }
    printf("done\n");
    return 0;
}

switch (C) vs match (Rust)

This is where the languages diverge most in control flow.

C: switch

/* switch_demo.c */
#include <stdio.h>

int main(void)
{
    int day = 3;

    switch (day) {
    case 1: printf("Monday\n");    break;
    case 2: printf("Tuesday\n");   break;
    case 3: printf("Wednesday\n"); break;
    case 4: printf("Thursday\n");  break;
    case 5: printf("Friday\n");    break;
    case 6: printf("Saturday\n");  break;
    case 7: printf("Sunday\n");    break;
    default: printf("Invalid\n");  break;
    }

    return 0;
}

Caution: Forgetting break in a C switch causes fallthrough -- execution continues into the next case. This is a legendary source of bugs.

Rust: match

// match_demo.rs
fn main() {
    let day = 3;

    let name = match day {
        1 => "Monday",
        2 => "Tuesday",
        3 => "Wednesday",
        4 => "Thursday",
        5 => "Friday",
        6 | 7 => "Weekend",
        _ => "Invalid",
    };
    println!("day {} is {}", day, name);
}

Key differences: no fallthrough, exhaustive (compiler rejects non-exhaustive matches), and match is an expression that returns a value.

Pattern matching with ranges and guards

// match_patterns.rs
fn main() {
    let score = 85;

    let grade = match score {
        90..=100 => "A",
        80..=89  => "B",
        70..=79  => "C",
        60..=69  => "D",
        0..=59   => "F",
        _        => "Invalid",
    };
    println!("score {} = grade {}", score, grade);

    let temp = 37;
    let status = match temp {
        t if t > 100 => "boiling",
        t if t == 37 => "body temperature",
        t if t > 0   => "cool",
        _            => "freezing",
    };
    println!("{}C is {}", temp, status);
}

Destructuring in match

// match_destructure.rs
fn main() {
    let point = (3, -5);

    match point {
        (0, 0)     => println!("origin"),
        (x, 0)     => println!("on x-axis at x={}", x),
        (0, y)     => println!("on y-axis at y={}", y),
        (x, y)     => println!("point at ({}, {})", x, y),
    }
}

Rust Note: Rust's match can destructure tuples, structs, and enums, bind variables, use guards, and combine patterns. It is one of Rust's most distinctive features.

Combining Constructs: FizzBuzz

C

/* fizzbuzz.c */
#include <stdio.h>

int main(void)
{
    for (int i = 1; i <= 20; i++) {
        if (i % 15 == 0)      printf("FizzBuzz\n");
        else if (i % 3 == 0)  printf("Fizz\n");
        else if (i % 5 == 0)  printf("Buzz\n");
        else                   printf("%d\n", i);
    }
    return 0;
}

Rust

// fizzbuzz.rs
fn main() {
    for i in 1..=20 {
        match (i % 3, i % 5) {
            (0, 0) => println!("FizzBuzz"),
            (0, _) => println!("Fizz"),
            (_, 0) => println!("Buzz"),
            _      => println!("{}", i),
        }
    }
}

The Rust version uses tuple matching to handle all four cases cleanly.

Quick Knowledge Check

  1. What does if (x = 5) do in C? What happens in Rust?
  2. Can you use an integer as a condition in a Rust if statement?
  3. What happens if you omit the _ wildcard in a Rust match on an i32?

Common Pitfalls

  • Missing break in C switch. Every case falls through without it. Use -Wimplicit-fallthrough to catch this.
  • Using = instead of == in C conditions. if (x = 5) assigns 5 to x and always evaluates to true. Use -Wall to get a warning.
  • Non-exhaustive match in Rust. The compiler will reject it. Always include a _ wildcard or cover every variant.
  • Off-by-one in ranges. C's for (i = 0; i < n; i++) corresponds to Rust's 0..n (exclusive). Use 0..=n for inclusive.
  • No ++/-- in Rust. Use += 1 and -= 1. This is deliberate to avoid the confusion between prefix and postfix increment.
  • Forgetting that Rust's for consumes the iterator. Use &collection to borrow instead of consuming.