C++ Mistakes
Common mistakes in C++ programming.
CppQuiz #31 — Most vexing parse
Initialization, function declaration ambiguity, temporary object, most vexing parse.
1
2
Y y(X());
y.f();
Mistake:
I thought Y y(X()); constructs an object y.
Correct:
It declares a function y returning Y, taking one parameter of type “function taking no arguments and returning X”.
Function parameter types are adjusted, so the parameter becomes pointer-to-function:
1
2
Y y(X()); // function declaration
Y y(X (*)()); // adjusted form
So y.f(); fails because y is a function, not an object.
Why:
In a parameter list, X() can mean: function taking no arguments and returning X
C++ allows function declarations inside a function body. The function cannot be defined locally, but it can be declared there and defined later at namespace scope.
Function parameter adjustment:
1 2 int f(); // f is a function returning int int (*f)(); // f is a pointer to function returning intThese are different as normal declarations.
But as a function parameter, function type is adjusted to pointer-to-function type.
C++ also allows extra parentheses around declarators, so:
1 int i(int(x));is a function declaration equivalent to:
1 int i(int x);
Choosing rule
When a statement is syntactically ambiguous between:
- expression statement
- declaration statement
C++ chooses the declaration.
If the whole statement cannot syntactically be a declaration, it is an expression.
1
2
3
4
5
6
7
T(a); // declaration: T a;
T(c) = 7; // declaration: T c = 7;
T(e)[5]; // declaration: T e[5];
T(a)++; // expression
T(a)->m = 7; // expression
T(a, 5) << c; // expression
Inside a declaration, function declaration can beat object construction:
1
Y y(X()); // function declaration, not object construction
This is the classic most vexing parse.
Fix
Modern C++:
1
Y y{X{}};
Old-school:
1
Y y((X()));
Appendix: extra cursed examples
1
T(*g)(double(3));
This is a declaration, but not a function pointer declaration.
It is parsed like:
1
T* g(double(3));
So g is a pointer to T, initialized by double(3).
This may be semantically invalid, but parsing happens before semantic checking.
For nested function declarations:
1
T1 (*(*b)(T2(c)))(int(d));
Parse from the name outward:
- b is a pointer to function -
(*b)- taking parameter c of type T2 -
(*b)(T2(c)) - returning pointer to function -
(*(*b)(T2(c)))- taking parameter d of type int -
(*(*b)(T2(c)))(int(d)) - returning T1 -
T1 (*(*b)(T2(c)))(int(d))
- taking parameter d of type int -
- taking parameter c of type T2 -
Equivalent using aliases:
1
2
3
4
using Inner = T1 (*)(int);
using Outer = Inner (*)(T2);
Outer b;
But alone:
1
(*(*b)(T2(c)))(int(d));
this is an expression statement, because it does not start with a declaration specifier like T1.
Extreme standard example
1
2
3
T1(a) = 3,
T2(4),
(*(*b)(T2(c)))(int(d));
C++ first tries to parse this as a declaration starting with type T1:
1
2
3
T1 a = 3,
T2(4),
...;
Here: T2(4) is treated as declaring a variable named T2 of type T1.
Then the final part fails, because it needs T2 to be a type name, but T2 has already been treated as a variable name.
C++ does not first ask “does this make sense?”
If it can be parsed as a declaration, it is parsed as a declaration, even if the declaration is later ill-formed.
CppQuiz #??? — std::future::get() twice
std::future,std::promise, shared state, one-shot result, undefined behavior.
1
2
3
4
5
6
7
std::promise<int> p;
std::future<int> f = p.get_future();
p.set_value(1);
std::cout << f.get();
std::cout << f.get();
Mistake:
I thought f.get() can be called multiple times.
Correct:
The first call: f.get(); waits until the shared state is ready, retrieves the value, and releases the shared state.
So it prints: 1.
After that: f.valid() == false
Calling get() again on this std::future is undefined behavior.
Many implementations throw std::future_error, but the standard says UB.
Rule to remember
1
std::future<T> // get once only
If I need to read the result multiple times, use:
1
std::shared_future<T>
Extra future / promise rules
future::get() is one-shot
1
2
auto x = f.get(); // OK
auto y = f.get(); // UB
get() consumes the future’s shared state.
After get():
1
f.valid() == false
Calling most member functions except destructor, move assignment, share(), and valid() on an invalid future is UB.
future::wait() does not consume
1
2
3
f.wait(); // OK
f.wait(); // OK
f.get(); // consumes
wait() only waits until the result is ready.
get() is the consuming operation.
shared_future::get() can be called multiple times
1
2
3
4
std::shared_future<int> sf = f.share();
sf.get(); // OK
sf.get(); // OK
Use shared_future when multiple reads are needed.
future::share() transfers the state
1
std::shared_future<int> sf = f.share();
After share():
1
f.valid() == false
The shared state moves from future into shared_future.
promise::get_future() is one-shot
1
2
auto f1 = p.get_future(); // OK
auto f2 = p.get_future(); // throws std::future_error
A promise can create only one future for its shared state.
Calling get_future() again throws std::future_error.
promise::set_value() is one-shot
1
2
p.set_value(1); // OK
p.set_value(2); // throws std::future_error
A shared state can store only one result.
The result is either value or exception not both, and not multiple times.
promise::set_exception() stores an exception
1
2
3
4
5
try {
throw std::runtime_error("oops");
} catch (...) {
p.set_exception(std::current_exception());
}
Then:
1
f.get();
throws that stored exception.
Destroying an unset promise gives broken promise
1
2
3
4
5
6
7
8
std::future<int> f;
{
std::promise<int> p;
f = p.get_future();
} // promise destroyed without set_value / set_exception
f.get(); // throws std::future_error
If the promise dies before setting a value or exception, the future receives broken_promise.
CppQuiz #235 — copying std::initializer_list
std::initializer_list, backing array, shallow copy.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C {
public:
C() = default;
C(const C&) { std::cout << 1; }
};
void f(std::initializer_list<C> i) {}
int main() {
C c;
std::initializer_list<C> i{c};
f(i);
f(i);
}
Creating:
1
std::initializer_list<C> i{c};
creates a hidden temporary array of const C.
The element is copy-initialized from c, so C is copied once and prints 1.
But copying an initializer_list does not copy its elements.
So:
1
2
f(i);
f(i);
only copies the lightweight initializer_list object (with a pointer to the hidden array), not the underlying C.
CppQuiz #44 — object slicing vs reference polymorphism
Inheritance, virtual function, reference, object slicing.
1
2
3
4
5
6
7
X arr[1];
Y y1;
arr[0] = y1;
print(y1);
print(arr[0]);
Mistake:
I thought Y is always converted to X, so output is XX.
Correct output: YX
Why:
1
arr[0] = y1;
slices, because arr[0] is a real X object. Only the X part of y1 is copied; the Y part is lost.
So:
1
print(arr[0]); // X
But:
1
print(y1);
does not slice, because print takes a reference:
1
void print(const X& x)
A base reference binds to the original Y object without copying.
Inside print(y1):
- static type:
const X& - dynamic type:
Y
Since f() is virtual:
1
print(y1); // Y
Rule
1
2
3
4
5
X x = y; // slicing
arr[0] = y; // slicing
const X& r = y; // no slicing
const X* p = &y; // no slicing
Base object copies slice.
Base references/pointers preserve dynamic type.
CppQuiz #4 — floating-point literal type
Overload resolution, literal type,
float,double.
1
2
3
4
5
void f(float) { std::cout << 1; }
void f(double) { std::cout << 2; }
f(2.5);
f(2.5f);
Mistake:
I forgot that 2.5 has type double, not float.
Correct output:
21
Rule to remember
1
2
3
2.5 // double
2.5f // float
2.5L // long double
No suffix = double by default.
CppQuiz #259 — char + char integral promotion
Integral promotion, usual arithmetic conversions, overload resolution.
1
2
3
4
5
6
7
8
void f(unsigned int) { std::cout << "u"; }
void f(int) { std::cout << "i"; }
void f(char) { std::cout << "c"; }
char x = 1;
char y = 2;
f(x + y);
x + y is not char. Before +, both chars go through integral promotion.
Usually: char -> int so most systems print: i
But the standard allows a rare case where: char -> unsigned int so this may also print: u
Both can be standard-conforming. But it will not print: c
Why
For arithmetic operators like +, C++ applies usual arithmetic conversions.
Small integer types are promoted before arithmetic:
1
2
3
4
5
6
char
signed char
unsigned char
short
unsigned short
bool
usually become int.
So: x + y is not computed as char + char. It is computed after promotion.
CppQuiz #250 — C-style ellipsis vs variadic template
Templates, overload resolution, variadic function, parameter pack.
1
2
3
4
5
6
7
8
template<typename T>
void foo(T...) { std::cout << 'A'; }
template<typename... T>
void foo(T...) { std::cout << 'B'; }
foo(1);
foo(1, 2);
Mistake:
I thought both calls choose the first overload, so output is: AA
Correct output: AB
Key difference
This one:
1
2
template<typename T>
void foo(T...)
is not a variadic template. It means roughly:
1
void foo(T, ...)
One real parameter of type T, then old C-style ellipsis.
But this one:
1
2
template<typename... T>
void foo(T...)
is a real variadic template. It has a parameter pack.
foo(1)
Both overloads are viable:
1
2
foo<int>(int, ...) // first
foo<int>(int) // second
The ... consumes zero arguments here, so both match 1 exactly.
Since conversion ranking ties, C++ uses template partial ordering.
The first template has a fixed first parameter:
1
2
template<typename T>
void foo(T, ...)
The second is a pure pack:
1
2
template<typename... T>
void foo(T...)
A fixed first parameter is considered more specialized than a pure pack, so:
1
foo(1); // A
foo(1, 2)
First overload:
1
foo<int>(int, ...)
Second argument 2 has to go into C-style ellipsis.
Second overload:
1
foo<int, int>(int, int)
Both arguments match normally.
Normal parameter match beats ellipsis match, so it prints: B
Rule to remember
1
2
3
4
5
template<typename T>
void f(T...); // one T + C-style ellipsis
template<typename... T>
void f(T...); // variadic template parameter pack
C-style ellipsis is a weak fallback.
Parameter pack gives real typed parameters.
CppQuiz #??? — std::flat_map, std::min, dangling reference
std::flat_map,operator[], reference invalidation, unspecified argument order.
1
2
std::flat_map<int, int> map;
std::cout << std::min(map[1], map[2]);
Mistake:
I thought this was about the default-initialized values of map[1] and map[2], so it should print 0.
Correct:
The trap is reference invalidation.
std::min takes arguments by const reference:
1
const T& min(const T& a, const T& b);
So:
1
std::min(map[1], map[2])
binds references to the results of map[1] and map[2].
But the evaluation order of function arguments is unspecified.
If map[2] is evaluated first, it inserts key 2 and returns a reference to its value.
Then map[1] inserts key 1.
std::flat_map stores data in vectors by default, so insertion can move elements and invalidate references.
Since 1 < 2, inserting 1 before 2 invalidates the reference returned by map[2].
Then std::min compares using a dangling reference → undefined behavior.
std::map= node-based, references stable on insert
std::flat_map= vector-based, insert may invalidate references
Short note: std::flat_map in C++23
std::flat_map is like std::map, but stored in sorted contiguous containers instead of tree nodes.
Comparison:
| Container | Storage | Lookup | Insert/erase | Reference stability |
|---|---|---|---|---|
std::map | tree nodes | O(log n) | O(log n) | stable on insert |
std::flat_map | sorted vectors | O(log n) | O(n) | can be invalidated |
Use std::flat_map when:
- data is mostly built once, then queried many times
- map is small/medium sized
- iteration speed and cache locality matter
- memory overhead matters
- sorted order is needed
Avoid std::flat_map when:
- many random insert/erase operations
- you need stable references/iterators
- you store huge objects that are expensive to move
- you need node-based operations like extract/merge
std::map — insert/erase while iterating
std::map is node-based.
Insertion does not invalidate existing iterators/references:
1
2
3
for (auto it = mp.begin(); it != mp.end(); ++it) {
mp.insert({new_key, new_val}); // OK
}
But newly inserted elements may or may not be visited later, depending on key order.
Erasing current element: use returned iterator.
1
2
3
4
5
6
7
for (auto it = mp.begin(); it != mp.end(); ) {
if (bad(it->first)) {
it = mp.erase(it); // OK, moves to next
} else {
++it;
}
}
CppQuiz #222 — std::variant default initialization
std::variant, default constructor, first alternative,index().
1
2
std::variant<int, double, char> v;
std::cout << v.index();
Mistake / missing rule:
I got it right, but forgot that default-constructing a variant chooses the first alternative.
Correct output: 0
Why:
For:
1
std::variant<int, double, char> v;
the alternatives are:
index 0 -> int
index 1 -> double
index 2 -> char
Default constructor makes v hold a value-initialized first alternative:
1
int{} // 0
So v.index() returns: 0
So v.index() == 0.
CppQuiz #295 — temporary lifetime until full-expression
Temporary object, destructor,
c_str(), full-expression, dangling pointer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
char a[2] = "0";
struct a_string {
a_string() { *a='1'; }
~a_string() { *a='0'; }
const char* c_str() const { return a; }
};
void print(const char* s) { std::cout << s; }
a_string make_string() { return a_string{}; }
int main() {
a_string s1 = make_string();
print(s1.c_str());
const char* s2 = make_string().c_str();
print(s2);
print(make_string().c_str());
}
Mistake:
At first I missed the temporary and guessed 111.
Then I thought the temporary would be destroyed immediately after c_str() was used, so I guessed 100.
Correct output: 101
Why:
1
2
a_string s1 = make_string();
print(s1.c_str());
s1 is a real local object. It lives until the end of main, so this prints 1.
1
const char* s2 = make_string().c_str();
The temporary a_string lives until the end of this full-expression, meaning until the semicolon.
Then it is destroyed, setting a back to 0.
So:
1
print(s2); // 0
In real code, this is like storing std::string(...).c_str() → dangling pointer.
1
print(make_string().c_str());
Here the temporary is a function argument.
It lives until the whole function call finishes, not just until c_str() returns.
So during print(...), the temporary is still alive, and this prints 1.
Rule
Temporaries are destroyed at the end of the full-expression.
1
2
3
4
5
const char* p = make_string().c_str();
// temporary dies here, at semicolon
print(make_string().c_str());
// temporary dies after print returns
semicolon ends temporary lifetime
function argument temporary survives until function call ends
CppQuiz #226 — mutable, copy ctor, no implicit move
mutable, copy constructor, move constructor, overload resolution, destructor count.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct X {
X() { std::cout << "1"; }
X(X &) { std::cout << "2"; }
X(const X &) { std::cout << "3"; }
X(X &&) { std::cout << "4"; }
~X() { std::cout << "5"; }
};
struct Y {
mutable X x;
Y() = default;
Y(const Y &) = default;
};
Y y1;
Y y2 = std::move(y1);
Mistake:
I forgot three things:
mutablemakesxnon-const even throughconst Y&.- Declaring
Y(const Y&)meansY(Y&&)is not implicitly declared. - Copying
xcreates anotherX, so twoXobjects are destroyed.
Correct output: 1255
Why:
1
Y y1;
default-constructs y1.x, so prints: 1.
Then:
1
Y y2 = std::move(y1);
does not move.
std::move(y1) makes y1 an rvalue, but Y has no move constructor.
So it calls:
1
Y(const Y&)
During memberwise copy, it copies:
1
x
The parameter is const Y&, but x is mutable, so other.x is treated as non-const.
Therefore this overload wins:
1
X(X&) // prints 2
not:
1
X(const X&) // prints 3
At the end of main, both objects are destroyed:
- y2.x destroyed -> 5
- y1.x destroyed -> 5
So total: 1255
Rule
1
mutable X x;
means x can be accessed as non-const even inside a const Y&.
1
Y(const Y&) = default;
suppresses implicit move constructor generation.
So:
1
std::move(y1)
does not guarantee moving. It only casts to rvalue. If no move constructor exists, copy constructor can still be called.
CppQuiz #125 — static local variable in function template
Function template specialization, static local variable.
1
2
3
4
5
6
7
8
9
template <class T>
void f(T) {
static int i = 0;
std::cout << ++i;
}
f(1);
f(1.0);
f(1);
Mistake:
I thought there is only one static int i, so output is 123.
Correct output: 112
Why:
Each template specialization gets its own static local variable.
So C++ creates:
1
2
f<int> // has its own static i
f<double> // has its own static i
Step by step:
f(1) -> f<int> -> int i becomes 1
f(1.0) -> f<double> -> double i becomes 1
f(1) -> f<int> -> int i becomes 2
Each static local variable is unique to its function template specialization.
CppQuiz #148 — volatile read is a side effect
volatile, unsequenced evaluation, side effect, undefined behavior.
1
2
3
4
5
volatile int a;
int main() {
std::cout << (a + a);
}
Mistake:
I thought a is zero-initialized, so a + a prints 0.
Correct:
The program has undefined behavior.
Why:
a has static storage duration, so yes:
1
a == 0
But the real issue is volatile.
Reading a volatile object is a side effect.
So in:
1
a + a
there are two volatile reads of the same object.
For +, the left and right operands are unsequenced relative to each other.
So we have two unsequenced side effects on the same object → UB.
Rule
1
2
volatile int a;
a + a; // UB
because both reads are volatile side effects and are unsequenced.
Safer version
1
2
3
int x = a;
int y = a;
std::cout << x + y;
Now the volatile reads happen in separate full-expressions / statements, so they are sequenced.
Mental model
normal int read = just value computation
volatile int read = side effect
volatile means “the read/write itself matters”, usually for memory-mapped I/O, signals, hardware-ish stuff.
It is not a thread-safety tool. Use atomics for that.
CppQuiz #160 — virtual function + default argument
Virtual dispatch, default arguments, static type vs dynamic type.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct A {
virtual void foo(int a = 1) {
std::cout << "A" << a;
}
};
struct B : A {
void foo(int a = 2) override {
std::cout << "B" << a;
}
};
A* b = new B;
b->foo();
Mistake:
I knew virtual dispatch calls B::foo, but I thought the default argument also comes from B.
Correct output: B1
Why:
1
A* b = new B;
Inside:
- static type:
A* - dynamic type:
B
The function body is chosen by dynamic type, because foo is virtual: B::foo(...)
But the default argument is chosen by static type: A* b
So the default argument comes from:
1
A::foo(int a = 1)
Therefore, b->foo(); means:
1
b->foo(1); // calls B::foo with a = 1
Rule
- virtual function body -> dynamic type
- default argument -> static type
So avoid different default arguments in overridden virtual functions. It’s cursed.
CppQuiz #3 — floating to signed/unsigned overload ambiguity
Overload resolution, floating-integral conversion, ambiguity.
1
2
3
4
void f(int) { std::cout << 1; }
void f(unsigned) { std::cout << 2; }
f(-2.5);
Mistake:
I thought double -> unsigned is not accepted, so it must call:
1
f(int)
Correct:
The program is ill-formed because the overload call is ambiguous.
Why:
-2.5 has type:
1
double
Both overloads are viable:
1
2
double -> int
double -> unsigned
Both are floating-integral conversions.
They have the same conversion rank, so neither overload is better.
Therefore:
1
f(-2.5); // ambiguous
CppQuiz #284 — std::string::operator[] at size()
std::string, null terminator,operator[], bounds.
1
2
std::string out{"Hello world"};
std::cout << (out[out.size()] == '\0');
Mistake:
I might think:
1
out[out.size()]
is out of bounds / UB.
Correct output: 1
Why:
For std::string, operator[] is valid when:
1
pos <= size()
If:
1
pos == size()
it returns a reference to the null character:
1
char{} // value 0
And:
1
'\0'
also has value 0.
So:
1
out[out.size()] == '\0'
is true.
Rule
1
2
s[i] // valid for i < s.size()
s[s.size()] // valid, gives '\0'
But do not modify it to anything except '\0'.
1
s[s.size()] = 'x'; // UB
CppQuiz #122 — unsigned ll typedef trap
Typedef, declaration parsing, defining type specifier.
1
2
3
4
5
6
typedef long long ll;
void foo(unsigned ll) { std::cout << "1"; }
void foo(unsigned long long) { std::cout << "2"; }
foo(2ull);
Mistake:
I thought unsigned ll means unsigned long long.
Correct output: 2
Exact rule:
While parsing a decl-specifier-seq, if a type-name appears, it is treated as a type-name only if there was no previous defining type specifier in that same specifier sequence, except cv-qualifiers.
Here:
1
unsigned ll
unsigned is already a defining type specifier and means unsigned int.
So ll is no longer parsed as the typedef name. It is parsed as the parameter name.
Therefore:
1
void foo(unsigned ll)
means:
1
void foo(unsigned int ll)
not:
1
void foo(unsigned long long)
So the overloads are:
1
2
foo(unsigned int) // prints 1
foo(unsigned long long) // prints 2
2ull has type unsigned long long, so it calls the second overload.
Rule to remember
1
2
3
typedef long long ll;
unsigned ll; // unsigned int variable named ll
Typedef aliases are not macros. unsigned does not modify the alias.