We use optional cookies for analytics and affiliate attribution. Essential cookies and marketing measurement are always on. Read our privacy policy

We just launched our Black Friday sale. You can use code BF2026 to get 25% off all subscriptions, valid until Nov 30th. There is a limit of 100 redemptions.

Logo

The Top 10 Most Convoluted C++ Concepts Asked in Interviews

Ten corners of the language that trip up engineers who've written C++ for years.

The Top 10 Most Convoluted C++ Concepts Asked in Interviews
By Coding Jesus

Our top 30 C++ concepts post covered what interviewers ask about most. This one covers where strong candidates still go wrong. Every concept here has a rule that sounds simple and a consequence that doesn't.

For each one you get a minimal example, why the language behaves that way, and the traps interviewers like to set. If you can explain all 10 without a compiler open, you're ahead of most people who write C++ for a living.

1. The Static Initialization Order Fiasco

Globals in the same .cpp file are initialized top to bottom. Globals in different files are initialized in an unspecified order, which in practice means whatever order the linker happened to see the object files.

// venue.cpp
std::string g_venue = "NASDAQ TotalView-ITCH 5.0";

// prefix.cpp
extern std::string g_venue;
std::string g_prefix = "[" + g_venue + "] ";  // is g_venue constructed yet?

Build with venue.cpp first and g_prefix is [NASDAQ TotalView-ITCH 5.0] . Swap the two files on the command line and it's [] , because prefix.cpp read a string whose constructor hadn't run yet. That read is undefined behavior; an empty string is the lucky outcome.

The nastiest part: with a short string like "XNAS", GCC and Clang constant-fold the initializer and the bug vanishes. It shows up months later when someone makes the string longer than the small string buffer.

The fixes:

  • Make it constant-initialized. constexpr and constinit (C++20) objects are set before any dynamic initialization runs, so order can't bite. constinit won't compile if the initializer isn't a constant, which is the point.

  • Wrap it in a function-local static (the Meyers singleton). It's built on first use, and since C++11 that first use is thread-safe.

constinit std::string_view g_venue = "NASDAQ TotalView-ITCH 5.0";

const std::string& venue_name() {
    static const std::string name = "NASDAQ TotalView-ITCH 5.0";
    return name;
}

What gets tested: the three phases (zero, constant, dynamic initialization), why constinit exists separately from constexpr, and the guard check a function-local static costs on every call. Also the mirror-image bug: statics are destroyed in reverse order, so a logger used from another static's destructor may already be dead. A function-local static fixes construction order, not destruction order.

2. The Most Vexing Parse

The rule is simple: if a statement can be read as a declaration, it is a declaration. The consequences are not.

struct Timer { Timer(); };
struct Guard { explicit Guard(Timer); void arm(); };

Guard g(Timer());        // declares a function g, not an object
std::vector<int> v();    // also a function

g is a function that returns a Guard and takes a pointer to a function returning Timer. Call g.arm() and GCC tells you g is of non-class type Guard(Timer (*)()), which is technically a perfect error message and practically useless.

The version that actually ships bugs looks like this:

std::mutex book_mutex;

void update_book() {
    std::unique_lock<std::mutex>(book_mutex);  // locks nothing
    // ... mutate the order book unprotected
}

Those parentheses are allowed around a declarator, so this declares a new, default-constructed unique_lock named book_mutex that shadows the global. It compiles, it runs, and owns_lock() returns false.

The fixes: brace initialization (Guard g{Timer{}};), an extra set of parentheses (Guard g((Timer()));), or auto g = Guard(Timer());. For locks, always name them: std::scoped_lock lock(book_mutex);. Both GCC and Clang flag these with -Wvexing-parse under -Wall, so read your warnings.

What gets tested: spotting the function declaration, explaining what type it actually declared, and the related trap std::lock_guard<std::mutex>{book_mutex};, which locks and immediately unlocks because the unnamed temporary dies at the semicolon.

3. Type Punning and Strict Aliasing

Type punning means reading an object's bytes as a different type. The strict aliasing rule says you may only access an object through its own type (give or take const and signedness), or through char, unsigned char or std::byte. Break it and the optimizer is allowed to assume your two pointers never overlap.

int store(int* i, float* f) {
    *i = 1;
    *f = 2.0f;   // an int and a float can't alias...
    return *i;   // ...so this compiles to 'return 1'
}

At -O2, both GCC and Clang emit mov eax, 1 and never reload *i. Pass the same address as both arguments and you get back 1 from memory that now holds a float. Add -fno-strict-aliasing (the Linux kernel builds with it) and the reload comes back.

The classic offender is pulling the bits out of a float:

auto u1 = *reinterpret_cast<std::uint32_t*>(&f);   // UB: strict aliasing

union Pun { float f; std::uint32_t u; };
auto u2 = Pun{f}.

You're one step away from reading the rest.

Sign up free to read the rest of this post. No paywall, commitment, or strings attached.