56 Intermediate The Hundred
The Initializer List That Lied
priced before it was counted
struct Receipt {
int total;
int items;
Receipt(int n) : items(n), total(items * 12) {} // $12 apiece
};
void checkout(int n) {
Receipt r(n);
std::cout << r.items << " items, total $" << r.total << "\n";
}
int main() {
for (int n : {3, 7, 5})
checkout(n);
}
Run it. Do the totals add up?
Answer
Not one of them — every receipt is priced from the previous customer's basket. Typical output (GCC 11.5 on x86-64, no -O flag):
Why¶
Members are initialized in the order they are declared in the class, never in the order the
initializer list happens to write them. total is declared first, so total(items * 12) runs
first and reads items while it is still indeterminate — undefined behavior, so those numbers
are what happened here, not a promise. Each Receipt reuses the stack slot the last checkout
left behind and inherits that customer's count; the first call finds an untouched slot and reads
zero. At -O1 and above all three print $0 — same bug, quieter lie. -Wall alone catches it
twice (-Wextra adds nothing):
warning: 'Receipt::items' will be initialized after [-Wreorder]
warning: 'int Receipt::total' [-Wreorder]
warning: '*this.Receipt::items' is used uninitialized [-Wuninitialized]
Listing total first silences -Wreorder and changes the output by zero cents — that warning is about the misleading order, not the bug.
The fix¶
Feed both members from the parameter, and list them in declaration order so the code reads the way it runs:
If a member truly must be built from a sibling, declare that sibling above it.
Takeaway: declaration order is initialization order — the initializer list only says what the values are, never when they arrive.
Try it: g++ -std=c++17 -Wall main.cpp -o demo && ./demo, then again with -O2