← BackC++30 Q

C++

RAII, move semantics, and the modern standard

30 questions | entry to principal | answers written to be said out loud, not read

Entry

What is the difference between a reference and a pointer in C++?

A reference is an alias for an existing object, it has to be bound when it is created and it can never be rebound or be null, which makes it the safer default for a parameter I do not want to copy. A pointer is a variable holding an address, it can be null, it can be reassigned, and I have to check it before I dereference it. My rule is that I pass by const reference for read only parameters, and I only reach for a raw pointer when the thing is genuinely optional or when I am walking memory.

What does const correctness mean, and why do interviewers care about it?

Const correctness means marking everything that does not modify state as const, so member functions that only read get a const qualifier and parameters I only read get passed as a const reference. It lets the compiler catch accidental mutation instead of me finding it in code review, and it is what makes an object usable from a const context at all. The subtlety people miss is that const applies to what sits on its left, so const char* is a pointer to constant characters while char* const is a constant pointer, and mutable is the escape hatch for caching inside a const member function.

Walk me through what a constructor and a destructor actually do.

A constructor runs when an object comes into existence and its job is to leave that object in a valid state, and I initialize members in the member initializer list rather than assigning in the body, because assigning means a default construct followed by an assignment. A destructor runs deterministically when the object goes out of scope or is deleted, and that is where I release whatever the object owns. The 2 things I always mention are that members are destroyed in reverse declaration order, and that throwing out of a destructor terminates the program, so a destructor should not throw.

When do you use auto, and what does it actually deduce?

I use auto when the type is obvious from the right hand side or genuinely unspellable, like an iterator or a lambda, because it cuts noise and prevents accidental narrowing conversions. The thing to know is that plain auto deduction strips references and top level const, so binding auto to a const reference hands me a copy, and I have to write auto& or const auto& when I want to bind to the original. That is exactly the bug behind a range for loop that quietly copies every element, which is why I default to writing for (const auto& item : items).

What is RAII, and why is it the core idea of C++?

RAII means resource acquisition is initialization, so a resource is acquired in a constructor and released in the destructor of the object that owns it, and the lifetime of that object is what controls the resource. Because destructors run deterministically when scope exits, including while an exception is unwinding the stack, I get cleanup that cannot be forgotten and needs no finally block. Almost everything good in modern C++ is RAII wearing a different hat, std::vector for memory, std::lock_guard for a mutex, std::unique_ptr for ownership.

When do you use unique_ptr, shared_ptr, and weak_ptr?

std::unique_ptr is my default because it expresses single ownership, it costs the same as a raw pointer, and it moves rather than copies. I only use std::shared_ptr when ownership is genuinely shared and the last owner out should free the object, which is rarer than people assume, and I build it with std::make_shared so the control block and the object land in 1 allocation. std::weak_ptr is the non owning observer that breaks a reference cycle, and I have to call lock on it to get a usable shared pointer back, which also tells me whether the object is still alive.

Why does a base class need a virtual destructor?

If I delete a derived object through a base class pointer and the base destructor is not virtual, the behavior is undefined, and in practice only the base part is destroyed so the derived members leak. Making the destructor virtual means the delete dispatches through the vtable, runs the derived destructor first and then the base one. My rule is that any class meant to be inherited from polymorphically gets a public virtual destructor, and if it is not meant for polymorphic deletion I make the destructor protected and non virtual instead.

How do you pick between vector, list, map, and unordered_map?

I start with std::vector for basically everything, because contiguous storage means the cache actually works for me, and even inserting in the middle often beats a linked list in practice. I use a deque when I need to push on both ends, and I almost never reach for std::list, only when I need references to elements to stay stable and constant time splicing. For lookup, std::map is a balanced tree so it is ordered with logarithmic lookup, while std::unordered_map is a hash table with average constant time, and I pick the ordered one only when I really need sorted iteration.

Junior

How do lambda captures work, and where do they bite you?

A lambda is really a compiler generated function object, and the capture list decides what state it carries. Capturing by value with [=] copies at the point the lambda is created, capturing by reference with [&] binds to the original variable, and that is where the danger lives, because if the lambda outlives the scope it captured from I get a dangling reference and undefined behavior. I capture explicitly by name instead of using a blanket default, and for anything asynchronous I capture by value or move the object in with an init capture.

What is the rule of 0, 3, and 5?

The rule of 3 says that if I need a custom destructor, copy constructor, or copy assignment operator, I almost certainly need all 3, because the reason I needed 1 of them is that the class manages a raw resource. The rule of 5 adds the move constructor and the move assignment operator, since declaring any of the others suppresses the implicitly generated moves. The rule of 0 is what I actually aim for, which is writing no special member functions at all and letting members like std::unique_ptr or std::vector own the resource, so the compiler generated versions are correct for free.

What is the difference between copying and moving an object?

A copy duplicates the underlying resource, so copying a vector of 1 million elements allocates a second buffer and copies every element into it. A move steals the guts instead, the destination takes the pointer and the size, and the source is left in a valid but unspecified state, which for a vector means the allocation never happens at all. The rule I hold onto is that a move is only cheap if the type actually implements one, and a moved from object is still safe to destroy or assign to, it just is not safe to assume anything about what it holds.

How do virtual functions work under the hood?

When a class has any virtual function the compiler gives that class a vtable, a static array of function pointers, and every object of the class carries a hidden vptr pointing at it. A virtual call is then an indirect call through that table, so it costs a pointer chase and it normally cannot be inlined, and that is the price of runtime dispatch. I mark every override with override so the compiler catches a signature mismatch instead of silently creating a new function, and I mark a class final when I want the compiler to be able to devirtualize.

What is object slicing?

Slicing is what happens when I assign or pass a derived object by value into a base type, because the base copy constructor runs and copies only the base subobject, so the derived state and the polymorphic behavior are silently sliced off. The result is still a valid object, it just is not the one I thought I had, and virtual calls on it now resolve to the base version. That is why I pass polymorphic types by reference or by pointer, and a container of base objects held by value is the classic way this shows up in real code.

How does a template actually get compiled?

A template is not code until it is instantiated, it is a pattern the compiler stamps out once per distinct set of template arguments I actually use, at compile time. That is why template definitions normally live in the header, since the compiler has to see the body at the point of instantiation, and it is also why heavy template use inflates build times and object code size. The error messages are the other tax, because a mistake is only caught when instantiation happens, which is how you get 200 lines of diagnostics pointing at a line deep inside the standard library.

How does std::vector grow, and when do you call reserve?

A vector holds a contiguous buffer with a size and a capacity, and when a push back exceeds the capacity it allocates a bigger buffer, usually 1.5 or 2 times the old one, moves or copies every element across, and frees the old block. That geometric growth is what makes push back amortized constant time, but every reallocation invalidates every iterator and pointer into the vector. If I know roughly how many elements are coming I call reserve up front, which does 1 allocation instead of a chain of them, and I only use shrink_to_fit when I genuinely need the memory back.

How do you protect shared state between threads in C++?

I put the shared data behind a std::mutex and I never lock it by hand, I use std::lock_guard or std::scoped_lock so the lock releases on every exit path including an exception, which is RAII again. std::unique_lock is the flexible one for when I need to unlock early or hand the lock to a condition variable. If I need 2 mutexes at once I take them together with std::scoped_lock in a single call, because acquiring them in different orders in different places is exactly how a deadlock happens.

Senior

What is undefined behavior, and why is it worse than a crash?

Undefined behavior means the standard places no constraints at all on what the program does, so signed overflow, reading an uninitialized value, dereferencing a dangling pointer, or an out of bounds index do not just produce a wrong answer, they let the optimizer assume that path is unreachable and delete the check guarding it. That is why the same code looks fine in a debug build and then breaks in release, or breaks 6 months later when somebody bumps the compiler. I treat it as a correctness bug and lean on the sanitizers plus warnings as errors, because I cannot reason my way to a guarantee the standard refuses to give me.

What does std::move actually do?

Nothing at runtime. std::move is just a cast to an rvalue reference, it does not move anything by itself, it only marks the argument as eligible to be moved from so overload resolution picks the move constructor or the move assignment operator when one exists. If the type has no move operations it silently falls back to a copy, which is the classic surprise people hit. The other gotcha is that returning std::move(local) from a function is actively harmful, because it blocks copy elision, so I return the local by name and let the compiler do better than me.

What does a shared_ptr actually cost, and how do you leak with one?

Every shared pointer carries 2 pointers, 1 to the object and 1 to a control block holding the strong count and the weak count, and those counts are atomic, so every copy and every destruction is an atomic increment or decrement even in a single threaded program. That is real overhead in a hot loop, which is why I pass a reference to the object or a const reference to the pointer instead of copying it around. The leak case is a reference cycle, 2 objects each holding a shared pointer to the other never drop to a strong count of 0, and the fix is making 1 direction a weak_ptr.

What is the diamond problem in multiple inheritance, and how do you handle it?

The diamond problem happens when 2 classes both inherit from the same base and a 4th class inherits from both, so it ends up carrying 2 copies of that base subobject and every access to a base member is ambiguous. Virtual inheritance fixes it by making them share 1 base subobject, but it costs an extra indirection and it pushes responsibility for constructing that base up to the most derived class, which surprises people. Honestly I design around it, I inherit implementation from at most 1 class and use interfaces with no state or plain composition for everything else.

What are the exception safety guarantees, and which one do you aim for?

There are 4 levels. The basic guarantee says that if something throws, nothing leaks and every invariant still holds, even though the state may have changed. The strong guarantee says the operation either succeeds completely or leaves everything exactly as it was, which is what vector push back gives you, and it is usually implemented by doing the work on a copy and then a cheap swap. Then there is the no throw guarantee, and finally no guarantee at all, which I treat as a bug. I aim for basic everywhere and strong on anything a caller might retry.

What is iterator invalidation, and which containers bite you?

Iterator invalidation is when an operation on a container makes existing iterators, pointers, or references to its elements unusable, and touching one afterwards is undefined behavior. For a vector, any reallocation from a push back invalidates everything, and an erase invalidates from the erase point onward, which is why the erase remove idiom hands me back a new iterator I am supposed to use. Node based containers like map and list are much kinder, only iterators to the removed element die. The classic bug is erasing inside a loop and then incrementing a dead iterator, so I always assign the result back.

Why do people say to prefer standard algorithms over raw loops?

A named algorithm says what I am doing instead of how, so std::find_if or std::accumulate or std::sort reads as intent and cannot get the loop bounds or the off by one wrong. The library implementations are also better than what I would hand write, because they specialize on the iterator category and they vectorize. With C++20 ranges I can pass the container directly instead of a pair of iterators, and compose views lazily with the pipe operator, which finally makes the algorithm version shorter than the loop rather than longer.

What does constexpr mean, and how far can you push compile time work?

constexpr says a function or a variable can be evaluated at compile time when its inputs are constant expressions, so a lookup table or the hash of a literal string costs nothing at runtime. It is a can and not a must, the same function still runs normally at runtime with non constant arguments, while consteval is the stronger form that must run at compile time and constinit fixes static initialization order. What I get out of it is fewer magic numbers, work moved off the hot path, and mistakes caught by the compiler rather than by a test, at the cost of build time.

Staff

What does noexcept actually buy you, and where does it matter most?

noexcept is a promise to the compiler that a function will not throw, and if it throws anyway the runtime calls std::terminate instead of unwinding, so it is a hard commitment rather than a hint. Where it really matters is move operations, because a vector will only move its elements while reallocating if the move constructor is marked noexcept, and otherwise it copies them to preserve the strong exception guarantee. So a missing noexcept on a move constructor quietly turns a cheap reallocation into a full copy of the container, and that is a performance bug nobody catches in code review.

What is perfect forwarding, and how do forwarding references make it work?

Perfect forwarding is passing an argument through a wrapper function while preserving whether it was an lvalue or an rvalue and whether it was const, so the final callee sees exactly what the original caller passed. It works because a T&& parameter in a deduced template context is a forwarding reference rather than an rvalue reference, and reference collapsing turns an lvalue argument back into an lvalue reference. Then std::forward casts it back to the right category. The mistakes I look for are using std::move where forward belongs, which silently steals from an lvalue, and forwarding the same argument twice.

How do concepts change template code compared to SFINAE?

Before C++20 the way to constrain a template was SFINAE, usually std::enable_if buried in a return type or a default template argument, which worked but was write only code and produced an error page pointing at the wrong line. A concept is a named boolean predicate over types that I can put in a requires clause or straight into the template parameter, so the constraint is readable, reusable, and it takes part in overload resolution properly. The real payoff is the diagnostics, the compiler tells me the type failed a specific requirement instead of dumping a substitution failure.

When would you use std::async instead of std::thread?

std::thread is a raw OS thread I own, so I have to join or detach it before it is destroyed or the runtime calls terminate, and I have to build my own channel to get a result or an exception back out. std::async hands me a future instead, so the return value and any thrown exception come back at the get call, and the implementation may reuse a pool thread. The trap is that the future from std::async blocks in its own destructor until the task finishes, so ignoring the return value quietly turns the call synchronous. In production I usually want neither directly, I want a thread pool with a work queue I control.

Principal

Explain the C++ memory model and when you would reach for std::atomic.

The memory model defines what a data race is, 2 threads touching the same location with at least 1 write and no synchronization between them, and it says that is undefined behavior rather than merely a torn value. std::atomic gives me operations that are indivisible and, under the default sequentially consistent ordering, a single total order that every thread agrees on. Relaxed, acquire, and release orderings let me buy performance back by ordering only what I actually need, for example release on the store and acquire on the load to publish data safely. I stay on the default until a benchmark says otherwise, because a wrong ordering produces bugs that only show up on another architecture.

What is false sharing, and how do you find and fix it?

False sharing is when 2 threads write to different variables that happen to sit on the same cache line, so the hardware bounces that line between cores and I pay coherence traffic for data that is not actually shared. It usually shows up as a parallel loop that gets slower as I add threads, and I confirm it with a profiler looking at cache line contention rather than by staring at the code. The fix is to pad or align the per thread data out to its own cache line, or better, give each thread a local accumulator and combine once at the end. It is the same lesson as cache locality generally, in a hot loop the layout of the data matters more than the instruction count.

Fast recall

RAII = cleanup tied to lifetime | rule of 5 = destructor, copy, move members | std::move = cast to rvalue reference | unique_ptr = single owner, zero overhead | shared_ptr = atomically counted shared ownership | weak_ptr = non owning cycle breaker | vtable = runtime dispatch table | slicing = derived state copied away | noexcept = promises never to throw | constexpr = evaluated at compile time | concept = named constraint on types | false sharing = same cache line contention

BH·C++·github.com/bunlongheng/study