Skip to content
Laviyx Learn

Try

C++ course menu

Advanced C++ Interview Questions and Answers

Advanced C++ interview questions with answers: move semantics and std::move, perfect forwarding, lvalues and rvalues, copy elision, templates and concepts, constexpr, inline and the ODR, memory layout and padding, data races and atomics, and undefined behaviour.

Advanced8 min readUpdated
On this page

Senior and systems-programming interviews probe the parts of C++ that make it fast: value categories and moves, templates, compile-time computation, memory layout and concurrency. Precise vocabulary matters here.

Value categories and moves#

1. What does std::move actually do?#

Show solution

Nothing at run time. std::move(x) is a cast to an rvalue reference (static_cast<T&&>(x)). It tells overload resolution that x may be moved from, so a move constructor or move assignment is chosen instead of a copy. The move itself happens in that function. After a move, the source is in a "valid but unspecified" state.

C++
#include <iostream>
#include <string>
#include <utility>

int main() {
    std::string a = "a long string that will not use the small buffer";
    std::string b = std::move(a);
    std::cout << b.size() << ' ' << std::boolalpha << a.empty() << '\n';
}
Output
48 true

a being empty is what GCC, Clang and MSVC do in practice, but it is not guaranteed by the standard.

2. What are lvalues and rvalues?#

Show solution

Every expression has a value category. Simplified: an lvalue has an identity and can be on the left of = (a variable, *p, v[0]); a prvalue is a pure temporary value (42, a + b, a function returning by value); an xvalue is an object about to expire (std::move(x)). Rvalue references (T&&) bind to prvalues and xvalues, which is how move semantics detects objects that can be safely stolen from.

3. What is perfect forwarding?#

Show solution

A function template that passes its arguments on unchanged in value category: lvalues stay lvalues and rvalues stay rvalues. It uses a forwarding reference (T&& where T is deduced) plus std::forward<T>:

C++
#include <iostream>
#include <string>
#include <utility>

void take(const std::string&) { std::cout << "copy\n"; }
void take(std::string&&) { std::cout << "move\n"; }

template <typename T>
void wrapper(T&& arg) {
    take(std::forward<T>(arg));
}

int main() {
    std::string s = "hello";
    wrapper(s);                   // lvalue in, lvalue out
    wrapper(std::string("hi"));   // rvalue in, rvalue out
}
Output
copy
move

std::make_unique, emplace_back and std::thread's constructor all use perfect forwarding.

4. What is copy elision / RVO?#

Show solution

The compiler constructs a returned object directly in the caller's storage, skipping the copy or move. Since C++17 it is guaranteed when returning a prvalue (return Widget(...);). Returning a named local (return result;, NRVO) is usually elided too, and otherwise moved automatically. Writing return std::move(result); can block NRVO, so don't.

5. Why should move constructors be noexcept?#

Show solution

std::vector must keep its strong exception guarantee when it reallocates. If an element's move constructor might throw, a failure halfway through would leave elements in both buffers, so the vector copies instead (via std::move_if_noexcept). Marking moves noexcept lets containers move, which is much faster.

Templates and compile time#

6. What are C++20 concepts, and why are they useful?#

Show solution

Concepts are named compile-time requirements on template parameters:

C++
#include <concepts>
#include <iostream>

template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template <Numeric T>
T clamp_to_zero(T value) {
    return value < 0 ? T{0} : value;
}

int main() {
    std::cout << clamp_to_zero(-5) << ' ' << clamp_to_zero(2.5) << '\n';
}
Output
0 2.5

They give clear error messages at the call site, document intent, and allow overloading on constraints, replacing complex SFINAE tricks.

7. What is the difference between const and constexpr? What is consteval?#

Show solution
  • const: cannot be modified after initialization; the value may be known only at run time.
  • constexpr variable: must be initialized with a constant expression, so it is usable at compile time (array sizes, template arguments).
  • constexpr function: can run at compile time when given constant arguments, and also at run time.
  • consteval (C++20) function: must run at compile time.

8. What is the One Definition Rule (ODR), and what does inline mean today?#

Show solution

The ODR says that every function and variable used in a program must have exactly one definition, although some entities (inline functions, templates, classes) may be defined in several translation units as long as all definitions are identical. Today, inline mainly means "this definition may appear in multiple translation units" (for example, functions or C++17 inline variables defined in headers), not "please inline this call"; the optimizer decides that independently.

9. Why are templates usually defined in header files?#

Show solution

A template is instantiated when it is used with specific types, and the compiler needs the full definition at that point. If the definition is in a separate .cpp file, other translation units only see the declaration, and the linker reports undefined references. Alternatives are explicit instantiation for a known set of types, or C++20 modules.

Memory and performance#

10. What is struct padding, and how can you reduce it?#

Show solution

Members are aligned to their natural alignment, so the compiler inserts padding bytes between members and at the end:

C++
#include <iostream>

struct Unordered { char a; double b; char c; };   // 1 + 7 pad + 8 + 1 + 7 pad
struct Ordered { double b; char a; char c; };     // 8 + 1 + 1 + 6 pad

int main() {
    std::cout << sizeof(Unordered) << ' ' << sizeof(Ordered) << '\n';
}
Output
24 16

Ordering members from largest to smallest alignment reduces padding. (Exact sizes are implementation-defined; these are typical for 64-bit platforms.)

11. Why can std::vector beat std::list even for insertions?#

Show solution

std::list has O(1) insertion given an iterator, but finding the position is O(n) pointer chasing, with every node in a separate heap allocation scattered through memory. std::vector stores elements contiguously, so the CPU cache and prefetcher make linear scans and element shifts extremely fast. For small to medium sizes, and for anything that searches first, a vector usually wins. Measure before choosing a list.

12. What is false sharing?#

Show solution

When two threads write to different variables that sit on the same cache line (often 64 bytes), each write invalidates the line in the other core's cache, forcing slow transfers even though no data is actually shared. Fix it by padding or aligning per-thread data to separate cache lines (alignas(64) or std::hardware_destructive_interference_size).

Concurrency and correctness#

13. What is a data race, and how do you prevent one?#

Show solution

A data race is two threads accessing the same memory location concurrently, at least one writing, without synchronization. It is undefined behaviour. Prevent it with a std::mutex (via std::lock_guard/std::scoped_lock), with std::atomic for single variables, or by not sharing mutable data at all (each thread owns its data, and results are combined after join).

14. What is the difference between a mutex and an atomic?#

Show solution

An atomic makes individual operations on one variable indivisible, usually with a single CPU instruction and no blocking. A mutex protects an arbitrary section of code involving any number of variables, but threads waiting for it block. Use atomics for counters and flags, and mutexes for invariants that span several variables.

15. Give examples of undefined behaviour that optimizers exploit.#

Show solution
  • Signed overflow: the compiler may assume x + 1 > x is always true for int x, and remove an "overflow check" written that way.
  • Null dereference: after *p, the compiler may assume p != nullptr and delete a later null check.
  • Out-of-bounds access: a loop that reads a[i] for i <= n on an n-element array may be "optimized" in surprising ways.
  • Data races: the compiler may keep a shared variable in a register, so another thread's write is never seen.

Because the compiler assumes UB never happens, UB can affect code before and after the faulty line. Sanitizers (-fsanitize=address,undefined,thread) catch many cases.

Next steps#

Practise turning these ideas into code in Coding Challenges.

Finished this lesson?