C++ lambdas and move semantics interview questions, with answers
Lambdas and move semantics are the two C++11 features that changed how everyday C++ is written, and they are where interviews check that your C++ is current. The questions are about what a lambda really is and what it captures, what an rvalue reference binds to, what std::move actually does, and why a move constructor should be noexcept. Every answer below comes with code whose output was produced by compiling and running it.
The questions start with lambdas and their captures, move through value categories and moving, and end with forwarding and the promise that lets containers move instead of copy. Then take the free C++ diagnostic — ten questions across every C++ topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.What is a lambda in C++, and what does its capture list do?
In short: A lambda is an unnamed function object written inline; the capture list says which local variables it can use and whether it stores copies of them or references to them.
A lambda expression such as [x, &total](int n) { total += n * x; } creates an object of a unique, unnamed class whose operator() runs the body. Variables named in the square brackets become its data members: x is captured by value, a copy made when the lambda is created, and &total by reference. [=] and [&] capture everything used in the body by value or by reference, and [this] captures the current object's pointer. A lambda that captures nothing converts to a plain function pointer, which is how it can be passed to C APIs. The danger with reference captures is lifetime: a lambda returned from a function or stored for later must not hold references to locals that have already died.
int base = 7; int total = 0; auto add = [base, &total]( int n) { total += base + n; }; add(1); add(2); std::cout << total; // 17
2.What does mutable mean on a C++ lambda?
In short: By default a lambda's operator() is const, so it cannot modify its by-value captures; mutable removes that const, letting the lambda change its own copies between calls.
The class generated for a lambda has a const call operator, so a by-value capture behaves like a const member, and incrementing it inside the body does not compile. Declaring the lambda mutable, as in [n]() mutable { return ++n; }, makes the call operator non-const, and the captured copy then keeps its changes from one call to the next, which turns the lambda into a small generator, as below. The original variable outside is never affected, because the lambda owns a copy. Copying a mutable lambda copies its state too, so passing it by value to an algorithm may leave your copy unchanged. Captures by reference never needed mutable, since the reference itself is not modified.
int n = 0; auto next = [n]() mutable { return ++n; }; next(); next(); std::cout << next() << ' ' << n; // 3 0
3.What are lvalues and rvalues in C++, and what is an rvalue reference?
In short: An lvalue names an object with an identity you can refer to again; an rvalue is a temporary or a value about to expire; an rvalue reference, T&&, binds only to rvalues, marking objects whose resources can be taken.
Every expression has a value category. A variable name, *p or v[0] is an lvalue: it identifies an object that persists, and you can take its address. A literal, the result of a + b, or a function returning by value is a prvalue, a temporary. std::move(x) produces an xvalue, an lvalue that has been marked as expiring. An ordinary reference, T&, binds to lvalues; a const T& binds to anything; and an rvalue reference, T&&, binds only to rvalues, which is how overload resolution can choose a move constructor for a temporary and a copy constructor for a named object, as the two calls below show. Inside a function, a named rvalue reference parameter is itself an lvalue.
void take(const std::string&) { std::cout << "ref "; } void take(std::string&&) { std::cout << "rref "; } int main() { std::string s = "text"; take(s); take(s + "!"); std::cout << "\n"; } // ref rref
4.What does std::move actually do in C++?
In short: Nothing at run time: std::move is a cast to an rvalue reference, which lets overload resolution pick a move constructor or move assignment, where the real transfer happens.
std::move(x) is equivalent to static_cast<T&&>(x): it changes the value category of the expression, not the object, and generates no code. The move happens only if the cast expression is then used to initialise or assign an object whose type has a move constructor or move assignment operator, which typically steals the source's heap buffer instead of copying it. Afterwards the source is in a valid but unspecified state: it can be destroyed or assigned a new value, but you should not rely on its contents. That is why std::move on a variable you use again later is a bug, and why moving from a type without move operations quietly copies. The string below moved its buffer to b; checking a's size is legal, but its value is unspecified.
std::string a(40, 'x'); std::string b = std::move(a); std::cout << b.size() << "\n"; // 40
5.What is a move constructor, and when does the compiler generate one?
In short: A move constructor, T(T&&), builds an object by taking over a temporary's resources; the compiler generates one only if the class declares no copy operations, move assignment or destructor.
A move constructor receives an rvalue reference to a source that is about to be discarded, so it can take the source's pointers and handles instead of duplicating what they point to, and then leave the source empty and safe to destroy. For a class that owns a large buffer this turns an O(n) copy into a few pointer assignments. The compiler writes a memberwise move constructor for you when the class declares none of the copy constructor, copy assignment, move assignment or destructor, which is the rule-of-zero case, and moving a Holder below moves its vector. Declaring any of those suppresses the implicit move, and the class then silently copies, which is a common performance bug. = default brings the generated moves back.
struct Holder { std::vector<int> data; }; int main() { Holder a{{1, 2, 3}}; Holder b = std::move(a); std::cout << b.data.size(); } // 3
6.What is perfect forwarding, and what does std::forward do?
In short: Perfect forwarding passes arguments through a template to another function with their value category intact, using a forwarding reference, T&&, and std::forward<T>, so rvalues are still moved and lvalues still copied.
Inside a function, every named parameter is an lvalue, so a wrapper that simply passes its parameters on would turn every temporary into an lvalue and force a copy. A function template parameter declared T&& where T is deduced is a forwarding reference: it binds to both lvalues and rvalues, and T records which it was. std::forward<T>(arg) then casts arg back to an rvalue only if it was one, so the inner call sees exactly what the outer call received, as the wrapper below shows. That is how std::make_unique, emplace_back and std::thread pass constructor arguments through without extra copies. Unlike std::move, which always casts to an rvalue, std::forward is conditional.
void sink(const std::string&) { std::cout << "lvalue "; } void sink(std::string&&) { std::cout << "rvalue "; } template <typename T> void relay(T&& arg) { sink(std::forward<T>(arg)); } int main() { std::string s = "x"; relay(s); relay(std::string("y")); std::cout << "\n"; } // lvalue rvalue
7.Why should a C++ move constructor be noexcept?
In short: Because std::vector moves elements during reallocation only if the move constructor cannot throw; otherwise it copies them, to keep its strong exception guarantee.
When a vector grows, it must transfer its elements to a new block. If it moved them and a move threw half-way, the old block would already be partly emptied and the vector could not roll back, breaking push_back's strong exception guarantee. So the library uses std::move_if_noexcept: it moves only if the move constructor is declared noexcept, or if the type cannot be copied at all, and otherwise copies. A move constructor written without noexcept therefore makes every reallocation copy, silently. Generated move constructors are noexcept whenever the members' moves are, and = default keeps that, so the problem mostly arises with hand-written ones. Below, reserve reallocates and the element is moved; delete the noexcept and the same program prints copied.
struct A { A() = default; A(const A&) { std::cout << "copied "; } A(A&&) noexcept { std::cout << "moved "; } }; int main() { std::vector<A> v(1); v.reserve(4); std::cout << "\n"; } // moved
How the diagnostic asks it
One question from the C++ bank, exactly as a sitting would show it. The bank has 4 on lambdas & move semantics and 30 across C++.
What does this C++ code print?
int x = 1; auto byVal = [x] { return x; }; auto byRef = [&x] { return x; }; x = 5; std::cout << byVal() << byRef();
- 155
- 215correct
- 311
- 451
[x] captures by value: the lambda stores its own copy of x, taken when the lambda is created, while x is 1, so byVal() returns 1 however x changes later. [&x] captures by reference: the lambda refers to x itself, so it sees the new value, 5. The output is 15. 55 treats both captures as references, 11 treats both as copies, and 51 swaps them. A by-value capture is also read-only inside the lambda unless the lambda is declared mutable, and a by-reference capture must not outlive the variable it refers to.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 C++ questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.