C++ Programming
Chapter: 5.2 C++ Programming Level: 5 — Mastery (cross-domain: build / prove / analyze) Time limit: 90 minutes Total marks: 60
Instructions: Answer all three questions. Code must compile under C++20. Where a proof or complexity argument is requested, state assumptions explicitly. Partial credit is awarded for correct reasoning even if code is incomplete.
Question 1 — RAII, Rule of Five, and Move Semantics on a Numeric Buffer (24 marks)
You are implementing a small linear-algebra primitive: a dynamically allocated Vec of double used to store an -dimensional vector, supporting the Euclidean norm .
(a) Implement a class Vec owning a raw double* data_ and std::size_t n_ that satisfies the Rule of Five: destructor, copy constructor, copy assignment, move constructor, move assignment. Provide a parameterized constructor Vec(std::size_t n) (zero-initialized) and an operator[] (both const and non-const). Your move operations must be noexcept and must leave the moved-from object in a valid, destructible state. (10)
(b) Add a member double norm() const computing using std::accumulate with a lambda. Explain why norm() is marked const and what would break in its signature if data_ were declared const double* vs double* const. (5)
(c) Consider:
Vec make() { Vec v(3); v[0]=3; v[1]=4; return v; }
Vec a = make();
Vec b = a; // (i)
Vec c = std::move(a); // (ii)State exactly which special member runs at lines (i) and (ii), whether copy elision applies to Vec a = make();, and the value of a.norm() after line (ii). Justify. (4)
(d) Prove that with your move constructor marked noexcept, a std::vector<Vec> reallocation during push_back will move rather than copy its elements, and explain the strong-exception-safety reason the standard library requires noexcept here. (5)
Question 2 — Templates, Concepts, and a Compile-Time Fold (20 marks)
(a) Write a variadic function template sum(args...) returning the sum of all arguments using a fold expression. Then constrain it with a C++20 concept Addable such that the template only participates in overload resolution when every argument type supports operator+ and is convertible to a common type. (8)
(b) Provide a template<typename T> constexpr T dot(const std::array<T,N>& a, const std::array<T,N>& b) (deduce N) computing the inner product . Make it usable in a constexpr context and give a static_assert verifying that dot of and equals . Show the arithmetic. (7)
(c) Explain SFINAE vs concepts for the constraint in (a): give one concrete diagnostic-quality difference in behavior when an unsupported type (e.g. a type with no operator+) is passed. (5)
Question 3 — Concurrency, the Memory Model, and a Producer/Consumer Proof (16 marks)
A producer thread computes partial sums of a physics simulation and hands a result to a consumer.
(a) Using std::atomic<int> with an explicit data field, implement a release–acquire handoff: producer writes data, then does flag.store(1, std::memory_order_release); consumer spins on flag.load(std::memory_order_acquire)==1 then reads data. Prove, using the happens-before relation, that the consumer is guaranteed to observe the producer's write to data (no data race, no torn/stale read). (8)
(b) Reimplement the same handoff with std::condition_variable, std::mutex, and std::unique_lock, avoiding busy-waiting and the lost-wakeup problem. Explain precisely why the wait must use a predicate (or a while loop) rather than a bare wait(). (5)
(c) State the three levels of exception-safety guarantee and classify: a function that (i) only reads, marked noexcept; (ii) modifies a container via copy-and-swap; (iii) appends to a std::vector that may throw bad_alloc mid-operation. (3)
Answer keyMark scheme & solutions
Question 1
(a) Rule of Five (10 marks)
#include <algorithm>
#include <cstddef>
#include <numeric>
#include <cmath>
#include <utility>
class Vec {
double* data_ = nullptr;
std::size_t n_ = 0;
public:
explicit Vec(std::size_t n) : data_(new double[n]{}), n_(n) {} // zero-init (1)
~Vec() { delete[] data_; } // (1)
Vec(const Vec& o) : data_(new double[o.n_]), n_(o.n_) { // copy ctor (2)
std::copy(o.data_, o.data_ + n_, data_);
}
Vec& operator=(const Vec& o) { // copy assign (2)
if (this != &o) { Vec tmp(o); swap(tmp); } // copy-and-swap
return *this;
}
Vec(Vec&& o) noexcept : data_(o.data_), n_(o.n_) { // move ctor (2)
o.data_ = nullptr; o.n_ = 0; // valid moved-from
}
Vec& operator=(Vec&& o) noexcept { // move assign (1)
if (this != &o) { delete[] data_; data_ = o.data_; n_ = o.n_;
o.data_ = nullptr; o.n_ = 0; }
return *this;
}
void swap(Vec& o) noexcept { std::swap(data_, o.data_); std::swap(n_, o.n_); }
double& operator[](std::size_t i) { return data_[i]; } // (1)
const double& operator[](std::size_t i) const { return data_[i]; }
std::size_t size() const { return n_; }
double norm() const;
};Marks: param ctor+dtor (2); copy pair (4); move pair noexcept + null-out (4).
Why: moved-from must be delete[]-safe → set data_=nullptr (deleting nullptr is legal). noexcept enables the vector-growth optimization (part d).
(b) norm() (5 marks)
double Vec::norm() const {
return std::sqrt(std::accumulate(data_, data_ + n_, 0.0,
[](double acc, double x){ return acc + x*x; }));
}constbecause computing the norm doesn't modify the vector; allows calling on constVec(2).const double* data_→ pointer to const data:operator[]non-const returningdouble&would fail to compile (can't return mutable ref) — makes the data immutable (2).double* const data_→ const pointer, mutable data: you can change elements but not reseat the pointer; copy/move assignment reassigningdata_would fail to compile (1).
(c) Special members & elision (4 marks)
Vec a = make();: copy elision / RVO — no move or copy runs (in C++17+ guaranteed for the prvalue return via NRVO is not guaranteed but the temporary→ais elided). The named localvreturn may use NRVO or the move ctor. (1)- Line (i)
Vec b = a;→ copy constructor (ais an lvalue). (1) - Line (ii)
Vec c = std::move(a);→ move constructor. (1) - After (ii),
ais moved-from:data_=nullptr, n_=0.a.norm()=accumulateover empty range =sqrt(0.0)= 0.0 (and is safe). (1)
(d) Proof move is used in vector growth (5 marks)
std::vector::push_back growth relocates existing elements. The standard (via std::move_if_noexcept) chooses the move constructor iff it is noexcept (or no copy ctor exists). (2)
Reasoning: during reallocation the vector must provide the strong guarantee — if relocating element throws, elements already moved into the new buffer would leave the container in a corrupted, unrecoverable state (moved-from originals are damaged). A throwing move cannot be rolled back, so the library falls back to copy (which leaves originals intact and can be rolled back). Marking move noexcept promises no throw → library safely moves. (3)
Question 2
(a) Variadic sum + Addable concept (8 marks)
#include <concepts>
#include <type_traits>
template<typename... Ts>
concept Addable = requires(Ts... a) {
(... + a); // fold: all support operator+
} && requires { typename std::common_type_t<Ts...>; };
template<typename... Ts>
requires Addable<Ts...>
constexpr auto sum(Ts... args) {
return (args + ...); // unary right fold
}Marks: fold expression (3); concept with requires on operator+ (3); common_type / convertibility (2).
(b) constexpr dot (7 marks)
#include <array>
#include <cstddef>
template<typename T, std::size_t N>
constexpr T dot(const std::array<T,N>& a, const std::array<T,N>& b) {
T s{};
for (std::size_t i = 0; i < N; ++i) s += a[i]*b[i];
return s;
}
static_assert(dot(std::array{1,2,3}, std::array{4,5,6}) == 32);Arithmetic: . (3) Marks: deduce N (2); constexpr loop (2); static_assert + shown arithmetic (3).
(c) SFINAE vs Concepts (5 marks)
- SFINAE: substitution failure in the immediate context silently removes the overload; if no viable overload remains you get a cryptic "no matching function" error, often buried in template instantiation depth. (2)
- Concepts: the compiler reports which named constraint was not satisfied (e.g. "constraint
Addable<...>not satisfied:(... + a)is invalid"), a far cleaner diagnostic; concepts also subsume/order overloads and are checked before instantiation. (3)
Question 3
(a) Release–Acquire handoff + happens-before proof (8 marks)
#include <atomic>
int data = 0;
std::atomic<int> flag{0};
void producer() {
data = 42; // (W1) plain write
flag.store(1, std::memory_order_release); // (R) release store
}
void consumer() {
while (flag.load(std::memory_order_acquire) != 1) {} // (A) acquire load
int x = data; // (R2) plain read, sees 42
}Proof (happens-before): (5)
- In the producer thread,
(W1)is sequenced-before the release store(R). - The acquire load
(A)in consumer reads the value written by release store(R)→ this forms a release–acquire synchronizes-with relation:(R)synchronizes-with(A). (A)is sequenced-before(R2).- By transitivity of the happens-before relation (sequenced-before ∘ synchronizes-with ∘ sequenced-before),
(W1)happens-before(R2). - Because a happens-before relation exists between the write and read of
data, there is no data race, and the memory model guarantees(R2)observes the value42. ∎
Marks: correct memory orders (3); the 4-step transitive happens-before chain (5).
(b) condition_variable version (5 marks)
#include <mutex>
#include <condition_variable>
std::mutex m; std::condition_variable cv; bool ready=false; int data2=0;
void producer() {
{ std::lock_guard<std::mutex> lk(m); data2 = 42; ready = true; }
cv.notify_one();
}
void consumer() {
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{ return ready; }); // predicate!
int x = data2; // safe
}Why predicate/while (3): wait() can return due to spurious wakeups, and a notification sent before the consumer entered wait would be lost. The predicate re-checks ready under the lock: if the condition already holds, wait returns immediately (no lost wakeup); on a spurious wakeup with the predicate false, it re-waits. A bare wait() handles neither.
Marks: unique_lock + notify (2); predicate reasoning (3).
(c) Exception-safety guarantees (3 marks)
- No-throw (nofail): never throws — (i). (1)
- Strong: commit-or-rollback, state unchanged on throw — (ii) copy-and-swap. (1)
- Basic: no leaks, invariants preserved but state may change — (iii)
vectorappend that may throwbad_alloc(vector itself gives strong forpush_back, but a mid-operation user append sequence gives only basic). (1)
[
{"claim":"dot of (1,2,3) and (4,5,6) equals 32","code":"a=[1,2,3]; b=[4,5,6]; result = (sum(x*y for x,y in zip(a,b)) == 32)"},
{"claim":"norm of (3,4,0) equals 5","code":"import math; v=[3,4,0]; result = (math.sqrt(sum(x*x for x in v)) == 5.0)"},
{"claim":"norm of empty moved-from vector is 0","code":"import math; v=[]; result = (math.sqrt(sum(x*x for x in v)) == 0.0)"},
{"claim":"sum fold of 1,2,3,4 equals 10","code":"result = (sum([1,2,3,4]) == 10)"}
]