Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
C++ Fundamentals
Before diving into complex topics, we must establish a solid foundation. C++ is a compiled language, which means your human-readable code is transformed into machine-readable instructions before it can run. This is different from interpreted languages like Python, where code is executed line-by-line.1. The Compilation Pipeline
Understanding how C++ code runs is crucial for debugging and optimization. It’s not magic; it’s a multi-step process.The Steps Explained
- Preprocessing: This happens before compilation. It handles directives starting with
#(like#include). It essentially copy-pastes code into your file. - Compilation: The compiler translates your C++ code into Assembly, a low-level human-readable language specific to your CPU architecture.
- Assembly: The assembler converts assembly code into Machine Code (binary), resulting in an “Object File” (
.oor.obj). - Linking: Finally, the linker combines your object files with external Libraries (like the C++ Standard Library) to create the final executable.
2. Anatomy of a C++ Program
Let’s dissect a standard C++ program line-by-line to understand what’s happening.Key Components
#include <iostream>: Tells the preprocessor to copy the contents of theiostreamfile here. This gives us access to input/output functionality.int main(): The function called by the OS when you run the program. It must return an integer (usually 0 for success, non-zero for error).std::: This is a namespace. It prevents name collisions. Think of it like a surname;std::coutis “cout from the Standard family”.cout: Character Output. Represents the console/terminal.endl: Inserts a newline character (\n) and flushes the buffer (forces output to appear immediately).
3. Variables & Data Types
C++ is statically typed, meaning variable types are checked at compile time. This catches many errors before you even run the program.Fundamental Types
| Type | Size (Typical) | Description | Range |
|---|---|---|---|
int | 4 bytes | Integer (Whole numbers) | -2B to +2B |
double | 8 bytes | Double-precision float (Decimals) | ~15 decimal digits |
float | 4 bytes | Single-precision float | ~7 decimal digits |
char | 1 byte | ASCII character | -128 to 127 |
bool | 1 byte | Boolean (Logic) | true or false |
void | 0 bytes | Empty/No type | N/A |
Initialization Styles
C++ offers multiple ways to initialize variables. Brace initialization (Uniform Initialization) is preferred in modern C++ because it prevents data loss.Type Deduction (auto)
Introduced in C++11, auto lets the compiler deduce the type from the initializer. This is useful for complex types but should be used judiciously.
Constants: const vs constexpr
When a value should never change, mark it const. When a value can be computed at compile time, use constexpr — this lets the compiler optimize aggressively.
const as a promise (“I won’t change this”) and constexpr as a guarantee (“this is known before the program even runs”). Prefer constexpr when possible — it catches errors earlier and enables compiler optimizations.
Compile-Time Constants Comparison
| Feature | #define | const | constexpr |
|---|---|---|---|
| Type-safe? | No (text substitution) | Yes | Yes |
| Debuggable? | No (replaced before compilation) | Yes (has a symbol) | Yes |
| Scope-aware? | No (global text replacement) | Yes (respects scope) | Yes |
| Computed at compile time? | Always (but no type checking) | Sometimes (compiler may optimize) | Guaranteed when possible |
| Usable in array sizes? | Yes | No (technically implementation-defined) | Yes |
| Can hold complex expressions? | Fragile (macro pitfalls) | Yes | Yes (functions too, since C++14) |
#define for constants in modern C++. Use constexpr when the value is computable at compile time. Use const when the value is determined at runtime but should not change after initialization.
4. Input & Output (I/O)
The<iostream> library provides streams for input and output. Think of streams as a flow of data bytes.
5. Control Flow
Control flow dictates the order in which statements are executed.Branching
Decisions are made usingif, else if, and else.
Switch Statement
Useful for checking a single variable against multiple discrete values (integers or enums).Loops
1. For Loop (Standard) Used when you know how many times you want to iterate.When to Use Which Loop
| Situation | Best Loop | Why |
|---|---|---|
| Iterating over a container (vector, map, array) | Range-based for | Cleanest syntax, no off-by-one errors, works with any iterable |
| Need the index during iteration | Standard for | Range-based for does not expose the index directly |
| Loop count unknown, condition-based | while | Reads naturally: “while this is true, keep going” |
| Must execute at least once | do-while | Checks condition after the first iteration |
| Iterating with complex step logic (e.g., two pointers) | Standard for | Full control over initialization, condition, and increment |
6. Functions
Functions break code into reusable blocks. They make code readable and maintainable.Declaration vs. Definition
In larger projects, we separate the declaration (telling the compiler a function exists) from the definition (the actual code).Parameter Passing
How you pass data to functions matters enormously for performance. Passing a 10MB vector by value copies all 10MB. Passing it by reference copies only 8 bytes (the pointer). That is the difference between a fast program and a slow one.- Pass by Value: Copies the data. Fine for small types (
int,double,char), but expensive for large objects. - Pass by Reference (
&): Passes the memory address. Fast, but the function can modify the original. - Pass by Const Reference (
const &): Fast and read-only. (The default choice for any non-trivial type).
Function Overloading
You can have multiple functions with the same name but different parameters. The compiler figures out which one to call based on the arguments. This is resolved entirely at compile time — there is zero runtime cost.Summary
- Compilation: Preprocessing -> Compilation -> Linking.
- Types: Statically typed. Use
autojudiciously. - Initialization: Prefer brace initialization
{}for safety. - Loops: Prefer range-based for loops for collections.
- Functions: Use
const type&for large objects to avoid copying.
Parameter Passing Decision Framework
| Type being passed | Size | Pass by | Example |
|---|---|---|---|
Primitives (int, double, bool, char) | 1-8 bytes | Value | void f(int x) |
| Small structs (2-3 members, all primitives) | Up to ~16 bytes | Value (often) or const& | void f(Point p) |
std::string, std::vector, custom classes | Varies, often large | const& (read-only) or & (modify) | void f(const std::string& s) |
| Sink parameters (the function will store/own the data) | Any | Value, then std::move inside | void f(std::string name) { member_ = std::move(name); } |
| Optional/nullable arguments | Any | Pointer (const T* or T*) | void f(const Config* cfg) — nullptr means “use defaults” |
Exercises
- Narrowing detection: Write a small program that tries to initialize an
intwith adoublevalue using all three initialization styles (=,(),{}). Predict which ones compile and which ones produce errors or warnings. Compile and verify. - Performance experiment: Write a loop that prints 100,000 lines using
std::endl, then again using"\n". Time both versions. What is the difference on your machine? (Hint: usestd::chrono::high_resolution_clock.) - The overload puzzle: Define
print(int),print(double), andprint(const std::string&). What happens when you callprint('A')? What aboutprint("hello")? Predict the behavior, then compile and check. Explain why each call resolves the way it does. - Const correctness: Take any small function you have written and add
consteverywhere it belongs — parameters, local variables, return types. Does it still compile? What did you learn about which values were actually being modified?
Interview Deep-Dive
What is undefined behavior in C++, and why is it more dangerous than a simple crash?
What is undefined behavior in C++, and why is it more dangerous than a simple crash?
- Undefined behavior (UB) means the C++ standard places no requirements on what the program does. It is not “implementation-defined” (where the compiler documents its choice) or “unspecified” (where the compiler picks one of several valid options). UB means the compiler is allowed to do literally anything — crash, produce wrong results, format your hard drive (the classic joke), or most insidiously, appear to work perfectly in testing and fail catastrophically in production.
- The real danger is that compilers actively exploit UB for optimization. If the compiler can prove that a code path involves UB, it is allowed to assume that path is never taken and optimize accordingly. A classic example: signed integer overflow is UB in C++. If you write
if (x + 1 > x), the compiler may optimize this toif (true)because it assumes signed overflow never happens. This means your “overflow check” is silently removed. The code compiles, passes tests with small values, and fails whenxisINT_MAXin production. - Common sources of UB that trip up even experienced developers: dereferencing a null pointer, reading uninitialized variables, accessing an array out of bounds, using an object after it has been moved from (beyond assignment or destruction), data races on shared mutable state without synchronization, and violating the strict aliasing rule (casting between unrelated pointer types).
- The mitigation strategy in production codebases: compile with sanitizers during testing (
-fsanitize=undefined,address,thread). UBSan catches undefined behavior at runtime with minimal overhead. ASan catches memory errors. TSan catches data races. At Google, these sanitizers are run continuously on the entire codebase and have caught thousands of bugs that passed all other tests.
- The strict aliasing rule says that you cannot access an object through a pointer of a different type (with a few exceptions like
char*andunsigned char*). The compiler relies on this rule to assume that pointers of different types do not alias the same memory, which enables powerful optimizations like reordering loads and stores. - A concrete violation:
float f = 3.14f; int i = *(int*)&f;— this casts afloat*to anint*and dereferences it. This is UB under strict aliasing. The compiler may reorder the read ofibefore the write off, or cache the value in a register and never actually read from memory. The “correct” way to do this type-punning isstd::memcpy(&i, &f, sizeof(i)), which the compiler recognizes and optimizes into a single register move — same performance, no UB. - In practice, GCC’s
-fstrict-aliasing(enabled at-O2and above) aggressively exploits this. Code that “works” at-O0can produce completely different results at-O2because the optimizer reorders memory accesses based on aliasing assumptions. This is one of the most common sources of “the optimized build behaves differently from the debug build” bugs.
Explain the difference between const, constexpr, and consteval. When would you use each?
Explain the difference between const, constexpr, and consteval. When would you use each?
constis a runtime promise: “this variable will not be modified after initialization.” The value can be determined at runtime — for example,const int x = getUserInput()is perfectly valid. The compiler enforces immutability but does not necessarily evaluate the value at compile time.constexpris a compile-time capability: “this value can be computed at compile time, and the compiler should try.” Aconstexprvariable must be initializable with a constant expression. Aconstexprfunction can be evaluated at compile time if all arguments are constant expressions, but it can also be called at runtime with runtime values. This dual nature is both its strength and a source of confusion.consteval(C++20) is a compile-time guarantee: “this function must be evaluated at compile time. Period.” If you call aconstevalfunction with a runtime argument, the program does not compile. This is useful for code generation, compile-time validation, and ensuring that certain computations never appear in the runtime binary.- Practical usage: use
constfor values determined at runtime that should not change (function parameters, loop invariants). Useconstexprfor values and functions that benefit from compile-time evaluation but might also be used at runtime (lookup tables, hash functions, mathematical constants). Useconstevalwhen compile-time evaluation is a correctness requirement, not just an optimization — for example, a function that generates a compile-time lookup table from a DSL string, where runtime evaluation would be meaningless. - A subtle point:
constexpron a variable means “must be a compile-time constant.” Butconstexpron a function means “may be evaluated at compile time.” These are different guarantees, and conflating them is a common source of confusion in interviews.
- A classic example is compile-time hash computation for string switch statements. C++ does not support
switchon strings, but you can write aconstexprhash function and switch on the hash:constexpr size_t hash(const char* s) { ... }thenswitch(hash(input)) { case hash("GET"): ... case hash("POST"): ... }. Thecaselabels are evaluated at compile time (they must be constant expressions), so the hash function runs during compilation and the runtime code is a simple integer comparison — no string comparison at all. - Another example: compile-time regular expression compilation. Libraries like CTRE (Compile-Time Regular Expressions) parse regex patterns at compile time using
constexprand generate optimized matching code. The regex"\\d{3}-\\d{4}"is parsed and converted into a state machine during compilation, so at runtime you get hand-tuned matching code with no regex interpretation overhead. Benchmarks show 10-100x speedups overstd::regex. - Compile-time lookup tables are also powerful. Instead of computing a CRC table at startup, you declare it
constexprand the compiler embeds the fully computed table in the binary’s read-only data section. Zero runtime initialization cost, and the table is shared across all instances of the program.
Walk me through what happens when you #include a header file. What problems does this cause at scale, and what are the modern solutions?
Walk me through what happens when you #include a header file. What problems does this cause at scale, and what are the modern solutions?
#includeis textual substitution performed by the preprocessor before compilation even begins. When you write#include <vector>, the preprocessor literally copies the entire contents of thevectorheader file into your source file. That header in turn includes other headers (<memory>,<algorithm>, internal implementation headers), which include more headers. A single#include <iostream>can expand to over 25,000 lines of code that the compiler must parse. This is called “transitive inclusion.”- At scale, this is devastating for compile times. In a large codebase with 10,000 source files, if each file includes 50 headers and each header expands to 20,000 lines, the compiler is parsing 200 million lines per translation unit times 10,000 units. Google reported that their C++ builds spend the majority of compilation time re-parsing the same headers across different translation units.
- Mitigation techniques in pre-C++20 codebases: (1) Forward declarations — declare
class Foo;instead of#include "Foo.h"when you only need a pointer or reference. This avoids pulling in Foo’s entire dependency tree. (2) The Pimpl idiom (pointer to implementation) — hide implementation details behind an opaque pointer so the header only needs forward declarations. (3) Precompiled headers (PCH) — the compiler parses commonly-used headers once and serializes the result. Subsequent compilations load the PCH instead of re-parsing. (4) Include-what-you-use (IWYU) tools analyze your code and remove unnecessary includes. - The modern solution is C++20 modules. Modules replace textual inclusion with semantic import. When you write
import std;, the compiler loads a pre-compiled module interface that contains only the exported declarations — no transitive includes, no macro leakage, no re-parsing. Early benchmarks from Microsoft (MSVC) showed 5-10x compile time improvements for module-heavy codebases. However, module adoption is still early: build system support (CMake, Bazel) is maturing, and many third-party libraries do not yet provide module interfaces.
- The ODR states that every entity (function, class, variable) must have exactly one definition across the entire program (for things with external linkage) or exactly one definition per translation unit (for things with internal linkage or defined in headers). Violating the ODR is undefined behavior — no diagnostic required, meaning the compiler is not obligated to warn you.
- The header model makes ODR violations easy because the same header can be included in multiple translation units with different preprocessor state. If file A defines
#define MODE 1before includingconfig.h, and file B defines#define MODE 2before including the sameconfig.h, andconfig.husesMODEto conditionally define a class layout, you get two different definitions of the same class in the same program. The linker silently picks one, and the other translation unit uses the wrong layout — corrupting memory at runtime. - This is not a theoretical concern. In production, ODR violations from inconsistent compiler flags (
-DNDEBUGin some files but not others), different include orders that change macro state, or inline functions with subtly different definitions across translation units are a real source of “impossible” bugs. Tools likeld’s--detect-odr-violationsand sanitizers can catch some of these, but they remain one of C++‘s most treacherous pitfalls.
Why does C++ have so many initialization syntaxes, and which should a modern C++ developer use by default?
Why does C++ have so many initialization syntaxes, and which should a modern C++ developer use by default?
- C++ has accumulated initialization syntaxes over 40 years of evolution, and each was added to solve a specific problem. Copy initialization (
int x = 5) comes from C. Direct initialization (int x(5)) was added for constructor calls. List initialization (int x{5}) was added in C++11 to provide a uniform syntax that works everywhere and prevents narrowing conversions. Designated initializers (Point p{.x = 1, .y = 2}) were added in C++20 for aggregate readability. - The modern default should be brace initialization
{}for most cases, because it prevents narrowing conversions (the compiler errors if you try to stuff adoubleinto anint), it works with aggregates, it works with constructors, and it avoids the “most vexing parse” problem whereWidget w()is parsed as a function declaration rather than a default construction. - The exception — and this is the gotcha that interviewers love: brace initialization interacts badly with
std::initializer_list. If a class has a constructor that takesstd::initializer_list, braces will prefer that constructor even when you intended a different one. The classic example:std::vector<int> v{10}creates a vector with one element (the value 10), not a vector with 10 default-constructed elements. To get the latter, you must use parentheses:std::vector<int> v(10). This is a well-known wart in the language. - My practical rule: use
{}by default for variables and aggregates. Use()when calling constructors whereinitializer_listambiguity exists (containers). Use= valuefor simple scalar initialization where readability is paramount and narrowing is not a concern. Consistency within a codebase matters more than any single rule.
- The most vexing parse is a C++ parsing ambiguity where a statement that looks like an object declaration is instead parsed as a function declaration. Example:
Widget w();— a developer intends to default-construct aWidgetnamedw, but the compiler parses this as a declaration of a function namedwthat takes no arguments and returns aWidget. This follows from C’s grammar rules that C++ inherited. - A more insidious example:
Widget w(Gadget())— the developer intends to construct aWidgetby passing a default-constructedGadget. But the compiler parsesGadget()as a function type (a function taking no arguments and returning aGadget), making the entire statement a declaration of a functionwthat takes a function pointer parameter. The code compiles without warning, butwis a function, not an object. Any attempt to usewas an object produces confusing errors. - Brace initialization solves this because braces cannot appear in function declarations.
Widget w{}is unambiguously an object construction.Widget w{Gadget{}}is unambiguously constructing a Widget with a Gadget argument. There is no grammar rule that could parse braces as a function declaration. This alone is a compelling reason to prefer braces.
What are the trade-offs between pass-by-value and pass-by-const-reference for function parameters in modern C++? Has the guidance changed with move semantics?
What are the trade-offs between pass-by-value and pass-by-const-reference for function parameters in modern C++? Has the guidance changed with move semantics?
- The traditional rule is simple: pass small types (primitives, small structs up to ~16 bytes) by value, and pass everything else by
const&. This avoids unnecessary copies of large objects while keeping the syntax clean for cheap-to-copy types. This rule is still correct for the vast majority of cases. - Move semantics introduced a nuance for “sink parameters” — parameters where the function will take ownership of the data (store it in a member, append it to a container). The modern idiom is: take the parameter by value, then
std::moveit into its final location. Example:void setName(std::string name) { name_ = std::move(name); }. If the caller passes an lvalue, this costs one copy plus one move. If the caller passes an rvalue (temporary), it costs two moves (which are near-free for types likestd::stringandstd::vector). - The alternative is providing two overloads: one taking
const std::string&(for lvalues) and one takingstd::string&&(for rvalues). This is marginally more efficient (one copy for lvalues, one move for rvalues — saving one move compared to the by-value approach) but doubles the number of overloads. For functions with N sink parameters, you need 2^N overloads, which is untenable. The by-value approach is the pragmatic default. - A subtle pitfall: do not blindly apply the by-value sink pattern to types where moves are expensive.
std::array<int, 10000>has no heap allocation, so “moving” it copies 40KB — same as a copy. For such types,const&plus explicit copy when needed is better. Always know the cost of a move for your type. - Another modern consideration: for generic code (templates), use forwarding references (
T&&withstd::forward) to get perfect forwarding. This gives you the efficiency of both overloads with a single function template, but the syntax and mental model are more complex.
- Perfect forwarding preserves the value category (lvalue vs rvalue) and const/volatile qualifiers of an argument as it is passed through a generic function. Without it, a template wrapper function would either always copy (if it takes by value) or always bind as lvalue (if it takes by
const&), losing the caller’s intent. - The mechanism: a forwarding reference
template<typename T> void wrapper(T&& arg)combined withstd::forward<T>(arg)inside the function body. Due to reference collapsing rules, if the caller passes an lvalue,Tdeduces toFoo&andT&&collapses toFoo&. If the caller passes an rvalue,Tdeduces toFooandT&&staysFoo&&.std::forward<T>then casts the argument back to its original category. - The canonical use case is
std::make_unique<T>(args...)andemplace_back(args...). These functions must forward constructor arguments to another function without adding copies. Without perfect forwarding,make_uniquewould need separate overloads for every combination of lvalue/rvalue arguments — combinatorially impossible for variadic argument lists. - The gotcha: a forwarding reference
T&&looks identical to an rvalue reference, but they are fundamentally different.void f(Widget&&)is an rvalue reference (only binds to rvalues).template<typename T> void f(T&&)is a forwarding reference (binds to anything). Confusing the two is a common interview mistake.