Advanced C++ Class Design: Dynamic Memory, Deep Copying, and Composite Structures
Component-Driven Interface Modeling
Building interactive systems often relies on composition. By nesting specialized objects within a manager class, developers can delegate functionality while preserving internal state.
#pragma once
#include <string>
#include <iostream>
#include <vector>
class Controller {
public:
explicit Controller(const std::string& identifier);
const std::string& identify() const;
void activate();
private:
std::string tag;
};
Controller::Controller(const std::string& identifier) : tag{identifier} {}
const std::string& Controller::identify() const { return tag; }
void Controller::activate() { std::cout << "Triggered [" << tag << "]\n"; }
#pragma once
#include <string>
#include <vector>
#include "Controller.hpp"
class Layout {
public:
explicit Layout(const std::string& header);
void render();
void shut_down();
void inject_element(const std::string& id);
void trigger_element(const std::string& id);
private:
bool verify_existing(const std::string& id) const;
std::string title;
std::vector<Controller> modules;
};
Layout::Layout(const std::string& header)
: title{header} {
inject_element("shutdown_handler");
}
void Layout::render() const {
std::cout << String(40, '*') << '\n';
std::cout << "Display: " << title << '\n';
for (size_t i = 0; i < modules.size(); ++i)
std::cout << (i + 1) << ". " << modules[i].identify() << '\n';
std::cout << String(40, '*') << '\n';
}
bool Layout::verify_existing(const std::string& id) const {
for (const auto& m : modules)
if (m.identify() == id) return true;
return false;
}
void Layout::inject_element(const std::string& id) {
if (verify_existing(id)) {
std::cout << "Duplicate detected: " << id << "\n";
return;
}
modules.emplace_back(id);
}
void Layout::trigger_element(const std::string& id) {
for (auto& m : modules)
if (m.identify() == id) { m.activate(); return; }
std::cout << "Missing target: " << id << '\n';
}
Returning a const std::string& from accessors eliminates unnecessary string duplication, though it requires guaranteeing the source object's lifetime exceeds the reference usage. Exposing member variables publicly bypasses encapsulation rules, allowing external code to mutate internal state unpredictably. Const-correct return types prevent accidental modification while avoiding copies.
Verifying Memory Separation in Standard Containers
The Standard Template Library allocates sequence elements independently during copy construction. This behavior guarantees that modifying the original container does not affect its replica.
#include <iostream>
#include <vector>
template<typename T>
void print_sequence(const std::vector<T>& src) {
if (src.empty()) { std::cout << '\n'; return; }
std::cout << src.at(0);
for (size_t i = 1; i < src.size(); ++i)
std::cout << ", " << src.at(i);
std::cout << '\n';
}
void validate_flat_copy() {
std::vector<int> alpha{42, 42, 42, 42, 42};
const std::vector<int> beta(alpha);
alpha.at(0) = -99;
print_sequence(alpha);
print_sequence(beta);
}
void validate_nested_copy() {
std::vector<std::vector<int>> group{{10, 20, 30}, {40, 50, 60, 70}};
const std::vector<std::vector<int>> mirror(group);
group[0].push_back(-1);
// Iterate and print nested structure
for (const auto& row : mirror) print_sequence(row);
}
Direct indexing ([]) skips bounds validation, triggering undefined behavior on out-of-range access. Conversely, .at() enforces range checks and throws std::out_of_range. When assigning alpha, the constructor performs a deep allocation, creating a separate heap block for beta. Mutating alpha leaves mirror unaffected, confirming independent memory ownership. Using const & parameters prevents value duplication while enforcing read-only semantics.
Constructing a Custom Dynamic Buffer
Managing raw arrays requires explicit lifecycle control. A robust wrapper handles allocation, deallocation, copying, and safe assignment.
#pragma once
#include <iostream>
#include <stdexcept>
#include <cstdlib>
class ResizableBlock {
public:
ResizableBlock();
ResizableBlock(size_t capacity);
ResizableBlock(size_t capacity, int init_val);
ResizableBlock(const ResizableBlock& other);
~ResizableBlock();
size_t count() const;
int& fetch(size_t idx);
const int& fetch(size_t idx) const;
ResizableBlock& load_from(const ResizableBlock& source);
int* begin(); int* end();
const int* begin() const; const int* end() const;
private:
size_t length_;
int* buffer_;
};
ResizableBlock::ResizableBlock() : length_{0}, buffer_{nullptr} {}
ResizableBlock::ResizableBlock(size_t cap) : length_{cap}, buffer_{new int[cap]} {}
ResizableBlock::ResizableBlock(size_t cap, int val) : length_{cap}, buffer_{new int[cap]} {
for (size_t i = 0; i < cap; ++i) buffer_[i] = val;
}
ResizableBlock::ResizableBlock(const ResizableBlock& other)
: length_{other.length_}, buffer_{new int[length_]} {
for (size_t i = 0; i < length_; ++i) buffer_[i] = other.buffer_[i];
}
ResizableBlock::~ResizableBlock() { delete[] buffer_; }
size_t ResizableBlock::count() const { return length_; }
int& ResizableBlock::fetch(size_t idx) {
if (idx >= length_) { std::cerr << "Out of bounds\n"; std::exit(1); }
return buffer_[idx];
}
const int& ResizableBlock::fetch(size_t idx) const {
if (idx >= length_) { std::cerr << "Out of bounds\n"; std::exit(1); }
return buffer_[idx];
}
ResizableBlock& ResizableBlock::load_from(const ResizableBlock& source) {
if (this == &source) return *this;
int* temp = new int[source.length_]();
for (size_t i = 0; i < source.length_; ++i) temp[i] = source.buffer_[i];
delete[] buffer_;
buffer_ = temp;
length_ = source.length_;
return *this;
}
int* ResizableBlock::begin() { return buffer_; }
int* ResizableBlock::end() { return buffer_ + length_; }
const int* ResizableBlock::begin() const { return buffer_; }
const int* ResizableBlock::end() const { return buffer_ + length_; }
The assignment operator must guard against self-assignment to avoid deleting valid data before reading it. Allocating temporary memory before releasing the old pointer ensures exception safety; if allocation fails, the object remains unmodified. Const overloads of access methods distinguish between mutable and immutable contexts. Iterator proxies returning raw pointers enable direct array manipulation while maintaining encapsulation.
Implementing Flat Storage for Multi-Dimensional Arrays
Two-dimensional structures can be optimized using contiguous one-dimensional allocations. Index mapping simplifies traversal and reduces fragmentation.
#pragma once
#include <iostream>
#include <cstring>
class TiledSurface {
public:
TiledSurface(int r, int c, double val = 0.0);
TiledSurface(int n, double val = 0.0);
TiledSurface(const TiledSurface& rhs);
~TiledSurface();
void populate(const double* data, size_t total);
void zero_out();
int height() const;
int width() const;
void visualize() const;
double& at(int r, int c);
const double& at(int r, int c) const;
private:
int rows_, cols_;
double* data_;
};
TiledSurface::TiledSurface(int r, int c, double val)
: rows_{r}, cols_{c}, data_{new double[r * c]} {
if (val != 0.0) std::fill(data_, data_ + r * c, val);
}
TiledSurface::TiledSurface(int n, double val) : TiledSurface(n, n, val) {}
TiledSurface::TiledSurface(const TiledSurface& rhs)
: rows_{rhs.rows_}, cols_{rhs.cols_}, data_{new double[rows_ * cols_]} {
std::memcpy(data_, rhs.data_, sizeof(double) * rows_ * cols_);
}
TiledSurface::~TiledSurface() { delete[] data_; }
void TiledSurface::populate(const double* data, size_t total) {
std::copy_n(data, total, data_);
}
void TiledSurface::zero_out() {
std::memset(data_, 0, sizeof(double) * rows_ * cols_);
}
int TiledSurface::height() const { return rows_; }
int TiledSurface::width() const { return cols_; }
void TiledSurface::visualize() const {
for (int r = 0; r < rows_; ++r) {
for (int c = 0; c < cols_; ++c)
std::cout << data_[r * cols_ + c] << ' ';
std::cout << '\n';
}
}
double& TiledSurface::at(int r, int c) {
if (r < 0 || r >= rows_ || c < 0 || c >= cols_)
{ std::cerr << "Boundary violation\n"; std::exit(1); }
return data_[r * cols_ + c];
}
const double& TiledSurface::at(int r, int c) const {
if (r < 0 || r >= rows_ || c < 0 || c >= cols_)
{ std::cerr << "Boundary violation\n"; std::exit(1); }
return data_[r * cols_ + c];
}
Flattening coordinates via row * columns + col eliminates pointer-to-pointer overhead. The copy constructor duplicates both dimensions and reserves fresh memory for pixel data. Clearing or updating one instance never impacts another because each maintains its own heap allocation. Using std::fill_n and std::copy_n streamlines bulk operations compared to manual loops.
Organizing Records with Search and Sort Routines
Aggregating structured entities benefits from algorithmic integration. Finding, removing, and ordering data becomes declarative when leveraging standard utilities.
#pragma once
#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
class ArchiveEntry {
public:
ArchiveEntry(const std::string& name, const std::string& contact);
const std::string& name() const;
const std::string& contact() const;
void show_details() const;
private:
std::string owner_name;
std::string phone_num;
};
ArchiveEntry::ArchiveEntry(const std::string& n, const std::string& p)
: owner_name{n}, phone_num{p} {}
const std::string& ArchiveEntry::name() const { return owner_name; }
const std::string& ArchiveEntry::contact() const { return phone_num; }
void ArchiveEntry::show_details() const {
std::cout << owner_name << " | " << phone_num << '\n';
}
#pragma once
#include "ArchiveEntry.hpp"
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
class Ledger {
public:
void register_entry(const std::string& nm, const std::string& ph);
void discard_entry(const std::string& nm);
void query_entry(const std::string& nm) const;
void list_all() const;
size_t entry_count() const;
private:
int find_position(const std::string& nm) const;
void enforce_order();
std::vector<ArchiveEntry> registry;
};
void Ledger::register_entry(const std::string& nm, const std::string& ph) {
if (find_position(nm) != -1) {
std::cout << nm << " is already recorded.\n";
return;
}
registry.emplace_back(nm, ph);
std::cout << nm << " added successfully.\n";
enforce_order();
}
void Ledger::discard_entry(const std::string& nm) {
auto pos = find_position(nm);
if (pos == -1) {
std::cout << nm << " not found for removal.\n";
return;
}
registry.erase(registry.begin() + pos);
std::cout << nm << " removed.\n";
}
void Ledger::query_entry(const std::string& nm) const {
auto pos = find_position(nm);
if (pos == -1) { std::cout << nm << " missing.\n"; return; }
registry[pos].show_details();
}
void Ledger::list_all() const {
for (const auto& item : registry) item.show_details();
}
size_t Ledger::entry_count() const { return registry.size(); }
int Ledger::find_position(const std::string& nm) const {
auto rev_it = std::find_if(registry.rbegin(), registry.rend(),
[&](const ArchiveEntry& e){ return e.name() == nm; });
if (rev_it != registry.rend())
return registry.size() - 1 - static_cast<int>(std::distance(registry.rbegin(), rev_it));
return -1;
}
void Ledger::enforce_order() {
std::sort(registry.begin(), registry.end(),
[](const ArchiveEntry& a, const ArchiveEntry& b) {
return a.name().compare(b.name()) < 0;
});
}
Index resolution uses reverse iterators combined with std::distance to locate the most recent match safely. Sorting employs a lambda comparator to arrange entries alphabetically. The erase method invalidates iterators, but since we hold an integer offset prior to removal, the operation remains safe. Const correctness throughout prevents accidental modification of stored strings outside the class scope. Query failures gracefully handle missing keys without crashing. All operation maintain internal consistency through deterministic state transitions.