Level 3 — ProductionC++ Programming

C++ Programming

45 minutes60 marksprintable — key stays hidden on paper

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-delete and 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) so delete[] nullptr is 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> (or std::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 a shared_ptr; does not affect the count and must be lock()ed to access. (1)

(b) — 4 marks

  • Why leak: a->next owns b and b->prev owns a, 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 prev to a weak_ptr. (2)
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node>   prev;   // break the cycle
};

(c) — 3 marks

  • use_count() of a's control block = 1. (1)
  • Justification: only the local variable a is a strong owner of that node; b->prev is now a weak_ptr and does not contribute to the count, so when a/b go 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::copy copies 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

  • wait must atomically release the mutex while sleeping and re-acquire it on wake; this requires unlock/lock control that only unique_lock exposes (a lock_guard cannot 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)"
  }
]