Bugs C++ lets through
Each mistake below is written twice: in C++, as a reviewer might miss it, and in Dazzle. Every output on this page was produced by the real compilers: the C++ by g++ and clang++ at -std=c++17 -O2, the Dazzle by dz. A script (scripts/capture.mjs in the site’s repository) records them, and the site’s build fails if a Dazzle message shown here stops matching what dz prints.
The first eight are undefined behaviour in C++: the program may do anything, and what it does changes with the compiler and the flags. The rest are mistakes C++ defines but does not stop. In Dazzle, each one is either a compile error or has one defined meaning, the same in every build and on every compiler.
Undefined behaviour in C++
Section titled “Undefined behaviour in C++”Reading past the end of an array
#include <cstdio>
int main() { int scores[3] = {10, 20, 30}; size_t i = 5; std::printf("%d\n", scores[i]);}It compiles: g++ 16 warns only with -Wall; clang 23 gives no warning, even with -Wall -Wextra.
$ ./a.out1567081552
$ ./a.out181667128
fn main() { let scores: [3]i32 = [10, 20, 30] let i: usize = 5 println!("{}", scores[i])}$ dz run --release .panic: index 5 is out of bounds for length 3 at main (src/main.dz:4:20)exit status 134 (aborted)
An index past the end reads whatever lies beyond the array. g++ warns here only because it can see that the index is the constant 5. Indexing an array or a slice in Dazzle is checked in every build, release included, and a bad index stops the program and names the line.
Spec §5.3, bounds checks →Reading memory nobody initialised
#include <cstdio>
struct Player { int health = 100; int score;};
int main() { int count; Player p; count += 1; std::printf("%d %d %d\n", count, p.health, p.score);}It compiles: g++ 16 and clang 23 warn only with -Wall.
$ ./a.out1 100 0
$ ./a.out1503104296 100 -2041975384
struct Player { pub health: i32 = 100 pub score: i32}
fn main() { let mut count: i32 let p: Player count += 1 println!("{} {} {}", count, p.health, p.score)}$ dz run --release .1 100 0
A local int and a field without an initialiser hold whatever was in memory. Dazzle has no uninitialised memory. A value starts at its type’s zero, and a field takes its declared default, so count starts at 0 and score is 0.
Using a pointer that may be null
#include <cstdio>#include <string_view>
struct Player { std::string_view name; int health = 100;};
Player* find(Player (&players)[2], std::string_view name) { for (Player& p : players) { if (p.name == name) return &p; } return nullptr;}
int main() { Player players[2] = {{"ada"}, {"bob"}}; Player* p = find(players, "eve"); std::printf("%d\n", p->health);}It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.
$ ./a.outexit status 139 (segmentation fault)
$ ./a.outexit status 40
struct Player { pub name: const &str = "" pub health: i32 = 100}
fn find(players: &[2]Player, name: &str) -> ^Player in players { for i in 0..2 { if players[i].name == name { return &players[i] } } return null}
fn main() { let players: [2]Player = [Player{ name: "ada" }, Player{ name: "bob" }] let p = find(&players, "eve") println!("{}", p.health)}$ dz check .error[DZ0528]: reaching through a `^T` with `.` is unsafe ┌─ src/main.dz:18:20 │ 18 │ println!("{}", p.health) │ ^^^^^^^^ outside an `unsafe` block │ = help: wrap it in `unsafe { … }` — nothing checks that the memory is still there. To use a pointer safely, turn it into a reference once with `p or …` (spec §5.2) 1 error emitted
find returns nullptr when there is no match, and nothing makes the caller check. The g++ build crashes, and the clang build prints nothing and exits with a meaningless status. In Dazzle, a ^Player cannot be read with . outside unsafe. The safe way is to turn it into a reference once, with let p = find(&players, "eve") or return, which handles the null case where it can happen.
Copying a struct that owns memory
#include <cstdio>
struct Buffer { int* data; Buffer() : data(new int[4]{}) {} ~Buffer() { delete[] data; }};
int main() { Buffer a; Buffer b = a; b.data[0] = 7; std::printf("copied, a sees %d\n", a.data[0]);}It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.
$ ./a.outcopied, a sees 7
struct Buffer { m_data: own ^mut i32 = null
Buffer() { m_data = unsafe { Allocators::Global.alloc(16, 4) as ^mut i32 } }}
impl Drop for Buffer { fn drop(&mut self) { Allocators::Global.free(unsafe { m_data as ^mut u8 }, 16, 4) }}
fn main() { let a = Buffer() let b = a println!("copied")}$ dz check .warning[DZ0406]: variable `b` is never used ┌─ src/main.dz:17:9 │ 17 │ let b = a │ ^ nothing reads this │ = help: if that is deliberate, name it `_b`; otherwise remove it error[DZ0403]: `Buffer` is move-only and cannot be copied ┌─ src/main.dz:17:13 │ 17 │ let b = a │ ^ this would copy it │ = help: it declares a `drop`, so copying it would release the same resource twice — write `move(…)` to transfer ownership, or give `Buffer` a `copy(other: &Self)` to make it copyable (spec §6.3) 1 error emitted
C++ generates a copy constructor that copies the pointer, so a and b share one array, and both destructors free it. This is the rule of three, broken silently. Both builds show the shared memory (a sees 7), and the second delete[] is a double free. This -O2 build let it pass unnoticed; built with -O0, the same program aborts. In Dazzle, a type with a drop is move-only unless it declares how to copy, so the copy is error DZ0403.
Signed overflow
#include <climits>#include <cstdio>
bool wraps(int x) { return x + 1 < x;}
int main() { std::printf("%s\n", wraps(INT_MAX) ? "true" : "false");}It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.
$ ./a.outfalse
fn wraps(x: i32) -> bool { return x + 1 < x}
fn main() { println!("{}", wraps(2147483647))}$ dz run --release .true
Signed overflow is undefined in C++, so an optimiser may assume x + 1 < x is never true and delete the check. Both builds print false for the largest int. Dazzle integers wrap in every build, signed ones included, so the comparison means what it says.
Dividing by zero
#include <cstdio>
int average(int total, int count) { return total / count;}
int main() { int players = 0; std::printf("average %d\n", average(120, players));}It compiles: g++ 14 and clang 19 give no warning, even with -Wall -Wextra.
$ ./a.outexit status 132 (illegal instruction)
$ ./a.outaverage 2005099624
fn average(total: i32, count: i32) -> i32 { return total / count}
fn main() { let players = 0 println!("average {}", average(120, players))}$ dz run --release .panic: division by zero at average (src/main.dz:2:12)exit status 134 (aborted)
Nobody has joined, so the average divides by zero. Neither compiler warns, and the two builds disagree: the g++ build stops on an illegal instruction, and the clang build prints an average that was never computed and carries on, with a different number on each run. In Dazzle, integer division by zero panics in every build and names the line, and a divisor that is a constant zero is a compile error. The rest of the arithmetic C++ leaves undefined is defined too: MIN / -1 wraps, a shift count is taken modulo the width, and a float converted to an integer saturates.
Falling off the end of a function
#include <cstdio>
int sign(int x) { if (x > 0) return 1; if (x < 0) return -1;}
int main() { std::printf("%d\n", sign(0));}It compiles: g++ 16 and clang 23 warn by default.
$ ./a.outexit status 132 (illegal instruction)
$ ./a.out-1
fn sign(x: i32) -> i32 { if x > 0 { return 1 } if x < 0 { return -1 }}
fn main() { println!("{}", sign(0))}$ dz check .error[DZ0405]: this function must return `i32` and does not on every path ┌─ src/main.dz:1:4 │ 1 │ fn sign(x: i32) -> i32 { │ ^^^^ some path reaches the end without a value │ = help: add a `return` at the end, or give the last `if` an `else` so every branch produces one 1 error emitted
Reaching the end of a function that returns a value is undefined behaviour in C++. Both compilers warn, but the program still builds. Here the g++ build stops on an illegal instruction, and the clang build prints -1 for sign(0). Dazzle’s flow analysis requires a value on every path.
Keeping a reference across a push_back
#include <cstdio>#include <vector>
int main() { std::vector<int> scores; scores.push_back(10); int& first = scores[0]; // a reference into the vector scores.push_back(20); // may move the array first += 1; std::printf("%d\n", scores[0]);}It compiles: g++ 14 and clang 19 give no warning, even with -Wall -Wextra.
$ ./a.out10
fn main() { let mut scores = List<i32>() scores.push(10) let first = &mut scores[0] // a reference into the list scores.push(20) // may move the array first += 1 println!("{}", scores[0])}$ dz check .error[DZ0413]: used after the memory it points into was killed: `scores@items` ┌─ src/main.dz:6:5 │ 5 │ scores.push(20) // may move the array │ --------------- this call kills `scores@items` 6 │ first += 1 │ ^^^^^ points into `scores@items` │ = help: a call, write, move or drop that can move or free memory ends every reference into it: use this before that happens, or get it again after (spec §6.7) 1 error emitted
first refers into the vector’s array, and the second push_back may move that array, so first += 1 writes into memory the vector has freed. Neither compiler warns, even with -Wall -Wextra, and both builds print 10: the increment went nowhere anyone will read it. In Dazzle, List::push says in its signature that it may move the list’s array (kills self@items), so the compiler knows the call ends first, and using it afterwards is error DZ0413, naming the call that ended it.
Mistakes C++ does not stop
Section titled “Mistakes C++ does not stop”Using a value after moving it
#include <cstdio>#include <string>#include <utility>#include <vector>
int main() { std::vector<std::string> names; names.push_back("ada"); std::vector<std::string> kept = std::move(names); std::printf("%zu %zu\n", kept.size(), names.size());}It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.
$ ./a.out1 0
fn main() { let mut names = List<String>() names.push(String("ada")) let kept = move(names) println!("{} {}", kept.len(), names.len())}$ dz check .error[DZ0401]: `names` was moved and cannot be used again ┌─ src/main.dz:5:35 │ 4 │ let kept = move(names) │ ----------- moved here 5 │ println!("{} {}", kept.len(), names.len()) │ ^^^^^ used here after the move │ = help: a moved-from value is left at its defaults, and reading it back would almost never be what was meant (spec §6.4) 1 error emitted
After std::move, a standard container is “valid but unspecified”: the C++ program may read names and gets whatever the library left there. In Dazzle, a moved-from value is reset to its defaults, and reading it again is error DZ0401, which shows where it was moved.
A conversion that loses the value
#include <cstdint>#include <cstdio>
int main() { int health = 300; uint8_t stored = health; std::printf("%d\n", stored);}It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.
$ ./a.out44
fn main() { let health: i32 = 300 let stored: u8 = health println!("{}", stored)}$ dz check .error[DZ0501]: expected `u8`, found `i32` ┌─ src/main.dz:3:22 │ 3 │ let stored: u8 = health │ ^^^^^^ this is `i32` │ = help: Dazzle has no implicit numeric conversions, not even widening (spec §5.1) — write `... as u8` 1 error emitted
Assigning an int to a uint8_t keeps the low eight bits, so 300 becomes 44, and neither compiler warns even with -Wall -Wextra: that needs -Wconversion. Dazzle has no implicit numeric conversions, not even widening. A conversion is written as u8, where a reader can see it.
Arguments evaluated in an unspecified order
#include <cstdio>
int say(const char* word) { std::puts(word); return 0;}
int both(int a, int b) { return a + b;}
int main() { both(say("first"), say("second"));}It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.
$ ./a.outsecond first
$ ./a.outfirst second
fn say(word: &str) -> i32 { println!("{}", word) return 0}
fn both(a: i32, b: i32) -> i32 { return a + b}
fn main() { let _ = both(say("first"), say("second"))}$ dz run --release .first second
C++ does not say which argument is evaluated first, and the two compilers disagree: the same source prints in a different order depending on which one built it. Dazzle evaluates arguments left to right, the receiver first, as they are written.
D-0049, evaluation order →A case that falls into the next
#include <cstdio>
void run(int command) { switch (command) { case 1: std::puts("save"); case 2: std::puts("quit"); break; }}
int main() { run(1);}It compiles: g++ 16 warns only with -Wall; clang 23 gives no warning, even with -Wall -Wextra.
$ ./a.outsave quit
fn run(command: i32) { switch command { case 1 { println!("save") } case 2 { println!("quit") } }}
fn main() { run(1)}$ dz run --release .save
A case without a break runs on into the next one, so command 1 saves and then quits. A Dazzle switch never falls through: each arm is a block, and only its own block runs. To match several values in one arm, write case 1, 2 { … }.
A missing case
#include <cstdio>
enum class Weapon { Sword, Bow, Staff };
int damage(Weapon w) { switch (w) { case Weapon::Sword: return 10; case Weapon::Bow: return 6; } return 0;}
int main() { std::printf("%d\n", damage(Weapon::Staff));}It compiles: g++ 16 warns only with -Wall; clang 23 warns by default.
$ ./a.out0
enum Weapon { Sword Bow Staff}
fn damage(w: Weapon) -> i32 { switch w { case Sword { return 10 } case Bow { return 6 } } return 0}
fn main() { println!("{}", damage(Weapon::Staff))}$ dz check .error[DZ0513]: `switch` on `Weapon` is not exhaustive ┌─ src/main.dz:8:5 │ 8 │ ╭ switch w { 9 │ │ case Sword { return 10 } 10 │ │ case Bow { return 6 } 11 │ │ } │ ╰─────^ `Staff` not covered │ = help: add the missing arms, or a `default` arm (spec §10.3) 1 error emitted
A switch on an enum that forgets a value compiles in C++, and that value runs none of the cases. A Dazzle switch on an enum or a union must cover every variant, and the error names the missing ones. A default arm says on purpose that the rest share one arm.
An error nobody looks at
#include <cstdio>
enum class SaveError { None, DiskFull };
SaveError save(const char* name) { if (name[0] == '\0') return SaveError::DiskFull; return SaveError::None;}
int main() { save("slot1"); std::puts("saved");}It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.
$ ./a.outsaved
enum SaveError : u32 { DiskFull = 1}
fn save(name: &str) !-> void { if name.len == 0 { return error(SaveError::DiskFull, "no room for {}", name) }}
fn main() { save("slot1") println!("saved")}$ dz check .error[DZ0510]: fallible call not consumed ┌─ src/main.dz:12:5 │ 12 │ save("slot1") │ ^^^^^^^^^^^^^ this may fail, and the failure is discarded │ = help: add an `or` clause: `or return` to propagate, `or <value>` to fall back, or `or ignore` to drop it deliberately (spec §9.7) 1 error emitted
A returned error code is just a value, and C++ lets the caller drop it. [[nodiscard]] helps only where someone wrote it. A Dazzle function that can fail says so with !->, and the call has to be handled in the same statement: or return, a fallback, or an explicit or ignore.
Read next
Section titled “Read next”- Dazzle vs C++: what changes for a C++ programmer, and what stays.
- Errors: how
!->andorwork. - Ownership: moves, copies and
drop.
