Lab 6 Assignment
Overview
This is an explore-and-reflect lab, like Labs 3 and 4. The goal is to learn by trying things, not to finish a polished homework. You will read a few short lessons, then run and change three small programs that cover the ideas the final emphasizes most: owning memory, copying vs moving, polymorphism, and how a growable container behaves. When you are done, you record a short video showing what you tried and explaining what you learned. There is no written report.
import std;. In this class, and in these labs, we use #include (for example #include <iostream>). Both are valid modern C++.unique_ptr and using them through a base class (polymorphism); Homework 2 was about owning a raw buffer by hand (deep copy, move, growth). This lab puts those same ideas in three short programs so you can compare them directly, which is what the transfer questions ask about.Where to run the code. You do not need anything installed. Paste each program into an online compiler and press Run: godbolt.org · onlinegdb.com · wandbox.org. To watch memory step by step, pythontutor.com (C++) is useful.
Reading
Read these lessons on learncpp.com. If a lesson ends with a Quiz time, do it and check the provided solution.
Asking the free store for memory and giving it back.
How cleanup runs automatically when an object goes away (RAII).
Why a class that owns memory needs its own copy, not a shared pointer.
Stealing resources instead of copying them, and what std::move does.
The single-owner smart pointer from Homework 1: it frees automatically and cannot be copied.
Writing one class that works for many types, like your Vector<T>.
Calling the right function through a base-class pointer, as in the Homework 1 item hierarchy.
Explore 1: two ways to own memory (rule of zero vs rule of five)
This is the central idea of the course and the most common transfer question. Owns holds a raw int buffer, so it defines its own copy and move (the rule of five). Wraps holds a std::vector, which already manages itself, so it needs none (the rule of zero). Predict what the program prints, then run it to check.
#include <iostream>
#include <vector>
#include <utility>
// Owns a raw buffer by hand -> rule of five (like your ct::Vector).
class Owns {
int* data_;
std::size_t size_;
public:
Owns(std::size_t n) : data_(new int[n]), size_(n) {
for (std::size_t i = 0; i < n; ++i) data_[i] = int(i);
}
~Owns() { delete[] data_; } // frees the buffer (RAII)
// deep-copy constructor: allocate a NEW buffer and copy the elements,
// so the copy does not share this object's buffer.
Owns(const Owns& other)
: data_(new int[other.size_]), size_(other.size_) {
for (std::size_t i = 0; i < size_; ++i) data_[i] = other.data_[i];
}
// move constructor: take other's pointer and size, then null out
// other.data_ so its destructor does not free your buffer.
Owns(Owns&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; other.size_ = 0;
}
int& operator[](std::size_t i) { return data_[i]; }
int* data() const { return data_; }
std::size_t size() const { return size_; }
};
// Owns nothing raw -> rule of zero (like HW1 Inventory).
class Wraps {
std::vector<int> v_;
public:
Wraps(std::size_t n) : v_(n) {}
// no destructor, copy, or move written by hand - the compiler's are correct
};
int main() {
Owns a(3);
Owns b = a; // deep copy: b gets its own buffer
b[0] = 99;
std::cout << "a[0]=" << a[0] << " b[0]=" << b[0] << "\n";
std::cout << "same buffer? " << (a.data() == b.data()) << "\n";
Owns c = std::move(a); // move: steals the buffer, leaves a empty
std::cout << "c[1]=" << c[1] << "\n";
Wraps w1(5);
Wraps w2 = w1; // just works - no special members needed
std::cout << "Wraps copied fine\n";
return 0;
}
- Predict first: after
b[0] = 99, what isa[0], and isa.data() == b.data()true or false? Run it and check. - The copy constructor allocates a new buffer. Explain what would go wrong if you deleted it and let the compiler copy
Ownsmember by member instead. (Hint: two objects, one buffer, two destructors.) - Which homework matches
Owns(rule of five), and which matchesWraps(rule of zero)? Why doesWrapsneed nothing?
You should see a[0]=0 (unchanged), b[0]=99, and same buffer? 0 - the copy is independent, and the move gives c the buffer. Wraps needed nothing because its std::vector member already owns and copies itself.
Explore 2: polymorphism through a base-class pointer
This is a small version of the Homework 1 item hierarchy. The items are held as unique_ptr<Item> and used through the base class. Predict the total, then run it.
#include <iostream>
#include <memory>
#include <vector>
struct Item {
virtual double value() const = 0; // pure virtual: every item defines it
virtual ~Item() = default; // virtual destructor
};
struct Equipment : Item {
double base;
Equipment(double b) : base(b) {}
double value() const override { return base; }
};
struct Borrowed : Item {
double value() const override { return 0.0; } // not ours -> no value
};
int main() {
std::vector<std::unique_ptr<Item>> items;
items.push_back(std::make_unique<Equipment>(1000.0));
items.push_back(std::make_unique<Borrowed>());
double total = 0;
for (const auto& p : items) // p is a unique_ptr<Item>
total += p->value(); // which value() runs?
std::cout << "total = " << total << "\n";
return 0;
}
- When you call
p->value()through the base, whose version runs - the derived ones, or the base's? What makes that happen? - What does
Borrowedadd to the total, and why? - Try to build an
Itemdirectly (Item x;). Why does it not compile? - Why does a base used through a pointer need a virtual destructor? (The
unique_ptrdeletes each item through anItem*.)
You should see total = 1000: the call dispatches to each item's own value(). Item is abstract, so it cannot be built directly.
Explore 3: size, capacity, and growth
Your ct::Vector keeps a buffer with more room than it is using. std::vector does the same, so you can watch it. Run this and read the numbers.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v; // starts empty
for (int i = 0; i < 20; ++i) {
v.push_back(i);
std::cout << "size=" << v.size()
<< " cap=" << v.capacity() << "\n";
}
std::vector<int> w;
w.reserve(50); // capacity only
std::cout << "after reserve: size=" << w.size()
<< " cap=" << w.capacity() << "\n";
w.resize(5); // element count
std::cout << "after resize: size=" << w.size()
<< " cap=" << w.capacity() << "\n";
return 0;
}
- Does capacity grow by one each push, or in jumps? What is the pattern? (Your
ct::Vectoruses 0, then 8, then doubles;std::vectoralso grows geometrically.) - After
reserve(50), what is the size? Afterresize(5), what is the size? State the difference betweenreserveandresizein one sentence. - Why keep capacity larger than size at all - what does it save?
You should see capacity jump (not increase by one) as the buffer fills, size grow by one each push, reserve change capacity but not size, and resize change the number of elements.
Record your reflection
When you have run and changed all three programs, record a short video (a few minutes is fine) and submit an unlisted YouTube link. In the video:
- Show your screen with the code you ran, and walk through what each of the three explores showed you.
- Explain, in your own words: the rule of zero vs the rule of five (Explore 1), how a call through a base pointer picks the right function (Explore 2), and the difference between size and capacity (Explore 3).
- We need to see your face along with the code. Use OBS or any screen-recording software; do not record with your phone.
There is no written report. Think about the answers before you record.