C++ Programming
Level: 3 — Production (from-scratch derivations, code-from-memory, explain-out-loud) Time limit: 45 minutes Total marks: 60
Question 1 — Rule of Five from memory (12 marks)
Write, from memory, a complete resource-owning class Buffer that manages a raw
heap array int* data_ of size size_t n_. Implement all five special
member functions explicitly (destructor, copy constructor, copy assignment, move
constructor, move assignment) plus a parameterized constructor Buffer(size_t n).
(a) Provide the full class code. (8) (b) In one sentence each, state why the copy operations must deep-copy and why the moved-from object must be left in a valid, destructible state. (2) (c) State what the Rule of Zero would do differently here and name the single standard-library member that would make Rule of Zero applicable. (2)
Question 2 — Smart pointer ownership reasoning (10 marks)
(a) Explain the ownership semantics of unique_ptr, shared_ptr, and weak_ptr
in one precise sentence each. (3)
(b) The following code leaks memory despite using shared_ptr. Explain why, then
fix it with a minimal change. (4)
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev;
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;(c) After your fix, state the use_count() of the control block that a points to,
just before a and b go out of scope. Justify the number. (3)
Question 3 — Templates + SFINAE / Concepts (12 marks)
(a) Write a function template sum that accepts any number of arguments and
returns their sum using a fold expression. Show the signature and body. (3)
(b) Write a function template print_if_integral that compiles only when its
argument type is an integral type. Give two versions: one using SFINAE
(std::enable_if) and one using a C++20 concept / requires. (6)
(c) Explain in one sentence what "substitution failure is not an error" means and why it enables overload selection rather than a hard compile error. (3)
Question 4 — STL + lambdas derivation (10 marks)
Given std::vector<int> v you must, using STL algorithms only (no raw loops):
(a) Compute the sum of squares of all elements. Write the single expression using
std::accumulate with a lambda. (4)
(b) Produce a new std::vector<int> containing only elements > 10, then state
which algorithm you used and why a plain std::copy cannot do this. (4)
(c) State the iterator category required by std::sort and why std::list cannot
be sorted with std::sort. (2)
Question 5 — Concurrency: producer/consumer from memory (10 marks)
Write, from memory, a thread-safe bounded-buffer style handshake where one
producer thread pushes a single int into a shared variable and one consumer
thread waits for it and prints it. Use std::mutex, std::condition_variable,
std::unique_lock, and a boolean flag.
(a) Provide the full code (both thread functions + main launching them). (7)
(b) Explain why condition_variable::wait must take a unique_lock and not a
lock_guard, and why a predicate overload of wait is preferred over a bare
wait. (3)
Question 6 — Explain-out-loud: memory model & noexcept (6 marks)
(a) Define the happens-before relationship and explain how
memory_order_release on a store paired with memory_order_acquire on a load
establishes it. (3)
(b) Explain why declaring a move constructor noexcept matters for
std::vector growth (reallocation) — what does vector do differently if the
move constructor is not noexcept? (3)
Answer keyMark scheme & solutions
Question 1 (12 marks)
(a) — 8 marks (deduct as noted for each missing/broken special member)
#include <algorithm>
#include <cstddef>
class Buffer {
std::size_t n_ = 0;
int* data_ = nullptr;
public:
explicit Buffer(std::size_t n) : n_(n), data_(new int[n]{}) {} // param ctor
~Buffer() { delete[] data_; } // destructor
Buffer(const Buffer& o) : n_(o.n_), data_(new int[o.n_]) { // copy ctor
std::copy(o.data_, o.data_ + o.n_, data_);
}
Buffer& operator=(const Buffer& o) { // copy assign
if (this != &o) {
int* tmp = new int[o.n_];
std::copy(o.data_, o.data_ + o.n_, tmp);
delete[] data_;
data_ = tmp;
n_ = o.n_;
}
return *this;
}
Buffer(Buffer&& o) noexcept : n_(o.n_), data_(o.data_) { // move ctor
o.data_ = nullptr;
o.n_ = 0;
}
Buffer& operator=(Buffer&& o) noexcept { // move assign
if (this != &o) {
delete[] data_;
data_ = o.data_;
n_ = o.n_;
o.data_ = nullptr;
o.n_ = 0;
}
return *this;
}
};Marks: param ctor (1), destructor (1), copy ctor deep copy (1.5), copy assign with self-check + strong-safety ordering (1.5), move ctor nulling source (1.5), move assign freeing old + nulling source (1.5).
(b) — 2 marks
- Deep copy: because copying only the pointer would give two objects owning the same
buffer, causing double-
deleteand aliasing bugs. (1) - Moved-from valid state: the destructor still runs on the moved-from object, so its
pointer must be
nullptr(and size 0) sodelete[] nullptris a safe no-op. (1)
(c) — 2 marks
- Rule of Zero: write none of the five; let the compiler generate them by holding the resource in a type that already manages it. (1)
- The member:
std::vector<int>(orstd::unique_ptr<int[]>). (1)
Question 2 (10 marks)
(a) — 3 marks
unique_ptr: sole/exclusive ownership; non-copyable, movable; deletes on scope exit. (1)shared_ptr: shared ownership via an atomic reference count; deletes when count hits 0. (1)weak_ptr: non-owning observer of ashared_ptr; does not affect the count and must belock()ed to access. (1)
(b) — 4 marks
- Why leak:
a->nextownsbandb->prevownsa, forming a reference cycle; each control block's count never reaches 0, so neither node is destroyed. (2) - Fix: make one direction non-owning — change
prevto aweak_ptr. (2)
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // break the cycle
};(c) — 3 marks
use_count()ofa's control block = 1. (1)- Justification: only the local variable
ais a strong owner of that node;b->previs now aweak_ptrand does not contribute to the count, so whena/bgo out of scope both counts reach 0 and both nodes are freed. (2)
Question 3 (12 marks)
(a) — 3 marks
template <typename... Ts>
auto sum(Ts... args) {
return (args + ...); // unary right fold over operator+
}Correct variadic pack (1), fold expression syntax (2).
(b) — 6 marks
SFINAE version:
template <typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
void print_if_integral(T x) { std::cout << x; }Concept version:
template <std::integral T>
void print_if_integral(T x) { std::cout << x; }
// or: void print_if_integral(std::integral auto x) { std::cout << x; }SFINAE correct (3), concept/requires correct (3).
(c) — 3 marks When template argument substitution produces an ill-formed type/expression in the immediate context, the compiler silently removes that candidate from the overload set instead of emitting an error — allowing another viable overload (or the constrained version) to be selected. (3)
Question 4 (10 marks)
(a) — 4 marks
int s = std::accumulate(v.begin(), v.end(), 0,
[](int acc, int x){ return acc + x*x; });Correct accumulate with init 0 (2), correct lambda accumulating squares (2).
(b) — 4 marks
std::vector<int> out;
std::copy_if(v.begin(), v.end(), std::back_inserter(out),
[](int x){ return x > 10; });- Algorithm:
std::copy_if. (2) - Why not
std::copy:std::copycopies every element unconditionally — it has no predicate to filter, so it cannot select only elements> 10. (2)
(c) — 2 marks
std::sort requires random-access iterators. std::list provides only
bidirectional iterators, so it cannot be passed to std::sort (use
list::sort() instead). (2)
Question 5 (10 marks)
(a) — 7 marks
#include <thread>
#include <mutex>
#include <condition_variable>
#include <iostream>
std::mutex m;
std::condition_variable cv;
int value = 0;
bool ready = false;
void producer() {
{
std::lock_guard<std::mutex> lk(m);
value = 42;
ready = true;
}
cv.notify_one();
}
void consumer() {
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{ return ready; }); // predicate guards spurious wakeups
std::cout << value << '\n';
}
int main() {
std::thread c(consumer);
std::thread p(producer);
p.join();
c.join();
}Shared state + mutex/cv/flag (2), producer sets under lock and notifies (2), consumer waits with predicate under unique_lock (2), threads launched & joined (1).
(b) — 3 marks
waitmust atomically release the mutex while sleeping and re-acquire it on wake; this requires unlock/lock control that onlyunique_lockexposes (alock_guardcannot be unlocked/relocked). (1.5)- The predicate overload re-checks the condition in a loop, protecting against spurious wakeups and lost/early notifications. (1.5)
Question 6 (6 marks)
(a) — 3 marks
Happens-before is a partial order on memory operations; if A happens-before B then A's
effects are visible to B. A release store synchronizes-with an acquire load that
reads the stored value; that synchronization establishes happens-before between all
writes sequenced before the release and all reads sequenced after the acquire. (3)
(b) — 3 marks
When vector reallocates, it moves elements to the new buffer only if the move
constructor is noexcept; otherwise (to preserve the strong exception guarantee)
it copies them instead, because a throwing move mid-transfer could leave the
container in an unrecoverable state. So a non-noexcept move ⇒ slower copies on growth. (3)
[
{
"claim": "Q2c: after fixing with weak_ptr for prev, strong use_count of node a is 1",
"code": "strong_owners_of_a = 1; result = (strong_owners_of_a == 1)"
},
{
"claim": "Q3a: unary right fold sum(1,2,3,4) equals 10",
"code": "result = (1+2+3+4 == 10)"
},
{
"claim": "Q4a: sum of squares of [1,2,3,4] via accumulate with x*x equals 30",
"code": "v=[1,2,3,4]; total=0\nfor x in v: total += x*x\nresult = (total == 30)"
},
{
"claim": "Q4b: copy_if of [3,15,7,20,11] keeping x>10 yields [15,20,11] (length 3)",
"code": "v=[3,15,7,20,11]; out=[x for x in v if x>10]; result = (out == [15,20,11] and len(out)==3)"
}
]