Chapter 3: Common Concepts
This chapter is a fast tour of SafeC's basic vocabulary: variables, types, operators, and control flow. If you already know C, most of this will read as "yes, obviously" — the goal is to flag the handful of places where SafeC's rules are stricter than C's, since those are exactly the spots where C muscle memory produces a compile error instead of the program you meant to write.
Variables
int age = 30;
auto name_len = 5; // type inferred from the initializer -- an int
const double pi = 3.14159;auto infers a variable's type from its initializer, nothing more exotic — it works in ordinary declarations and for-loop init clauses, with no flow analysis beyond that single initializer expression. const makes a binding immutable, exactly like C's const, checked at compile time.
One C habit that doesn't carry over: SafeC has no C-style multi-variable declarations. int a = 1, b = 2; doesn't parse — split it into two statements. The same applies inside a for loop's init clause: for (int i = 0, j = 10; ...) doesn't work either; declare j before the loop instead.
Types and conversions
SafeC's primitive types are the same set C gives you — int, long, long long, short, char, their unsigned variants, float, double, bool — with the same sizes on the platforms SafeC targets. Where SafeC differs sharply from C is implicit conversions: C will freely convert between an int and an unsigned long in a mixed expression (the "usual arithmetic conversions"), silently reinterpreting the value's meaning if the signs disagree. SafeC doesn't:
unsigned long size = 10UL * sizeof(int); // OK -- both operands unsigned long
// unsigned long bad = 10 * sizeof(int); // ERROR: int and unsigned long differsizeof returns unsigned long; a bare 10 is int. Multiplying them needs the same type on both sides, so the literal gets an explicit UL suffix — that's a binary-operator rule (both operands of +/*/... must already be the same type). Assigning or passing a value, on the other hand, allows one implicit conversion: widening — any smaller numeric type to a bigger one, including crossing from integer to float (int → double, not just int → long/float → double):
int x = 42;
double dx = x; // implicit: int -> double widens safely
long long big = 100LL;
int small = (int)big; // explicit cast required: this narrowsNarrowing (bigger → smaller, or float → integer) always needs an explicit cast — see Types for the full widening rule.
This shows up constantly once you're calling into libc functions whose signatures use size_t/unsigned long — get used to reaching for a UL suffix or an explicit cast the moment the compiler complains about two types "differing."
Literal syntax gaps
One literal form C has that SafeC doesn't: a leading 0 does not mean octal — 0777 is decimal 777, not octal 511, so if you actually need an octal value, compute it from hex/binary or write out the decimal value. Binary literals are supported, with a 0b/0B prefix just like hex's 0x/0X — see Literals for the full table:
unsigned int flags = 0b1100U; // 12
unsigned int mask = 0b1010U; // 10
printf("%u %u %u\n", flags & mask, flags | mask, flags ^ mask);
// 8 14 6Operators
Arithmetic, comparison, and logical operators are all exactly what you'd expect from C:
int a = 7;
int b = 3;
printf("%d %d %d %d\n", a + b, a - b, a * b, a / b); // 10 4 21 2
printf("%d\n", (a > b) && (b > 0)); // 1Bitwise operators (&, |, ^, ~, <<, >>) are also unchanged from C. What is new is a set of operators for explicit overflow behavior, since plain +/-/* on signed integers still carries C's undefined-behavior-on-overflow rule (unsigned still wraps, as in C):
int max = 2147483647; // INT_MAX
int wrapped = max +| 1; // wrapping add: -2147483648 (two's complement wrap)
int saturated = max +% 1; // saturating add: 2147483647 (clamped, doesn't wrap)+|/-|/*| always wrap (defined two's-complement behavior, useful for hashing, checksums, ring-buffer indices); +%/-%/*% always saturate (clamp to the type's min/max, useful for audio/signal processing and anywhere "closest representable value" beats "wrap around"). See Overflow Operators for the complete operator table and std::checked_mul_size/<stdckdint.h> for detecting overflow instead of resolving it a particular way.
Control flow
if/else, while, and C-style for all work exactly as in C:
int i = 0;
while (i < 3) {
printf("while: %d\n", i);
i = i + 1;
}
for (int j = 0; j < 3; j = j + 1) {
printf("for: %d\n", j);
}
int k = 5;
if (k > 10) {
printf("big\n");
} else if (k > 0) {
printf("small positive\n");
} else {
printf("non-positive\n");
}SafeC also has match — a pattern-matching statement/expression that covers the same multi-way-branch ground as C's switch without its fall-through footguns (each case is independent; no break needed to stop it from falling into the next one). A real C-style switch/case with fall-through is available too, for when you actually want it — see Control Flow — but match deserves its own space rather than a quick mention here: Chapter 7 covers it alongside enums and tagged unions, the two things it's most useful for matching against.
Next: Chapter 4 covers functions — declarations, parameters, and a few SafeC-specific attributes (pure, inline, must_use) that don't have a direct C equivalent.