Skip to content

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):

3 items, total $0
7 items, total $36
5 items, total $84

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:

Receipt(int n) : total(n * 12), items(n) {}

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

Open in Compiler Explorer ↗ Quiz this entry