CPP Series-10: Building a Complete Application
Introduction
In this final article, we will bring together everything we’ve learned throughout this series to build a complete, professional-grade C++ application. This project will integrate advanced C++ concepts such as object-oriented programming, templates, error handling, and multithreading, showcasing how they can be applied in a real-world scenario.
Application Overview
Our application will be a comprehensive Task Management system with a command-line interface (CLI). It will allow users to:
- Add, view, and complete tasks.
- Sort tasks by priority.
- Process tasks concurrently using a thread pool.
- Save and load tasks from a file, ensuring data persistence.
Application Architecture
- Task Management: Handles task creation, viewing, and processing.
- Concurrency Management: Manages task processing through multithreading.
- File I/O: Saves and loads tasks to/from a file, providing persistence.
- Command-Line Interface: A simple, user-friendly interface for interacting with the application.
Implementing the Application
Code: Task.h
#ifndef TASK_H
#define TASK_H
#include <iostream>
#include <string>
class Task {
private:
std::string name;
std::string dueDate;
int priority;
public:
Task(const std::string& name, const std::string& dueDate, int priority)
: name(name), dueDate(dueDate), priority(priority) {}
virtual ~Task() = default;
virtual void display() const {
std::cout << "Task: " << name << ", Due: " << dueDate << ", Priority: " << priority << std::endl;
}
const std::string& getName() const { return name; }
const std::string& getDueDate() const { return dueDate; }
int getPriority() const { return priority; }
};
class BasicTask : public Task {
public:
BasicTask(const std::string& name, const std::string& dueDate, int priority)
: Task(name, dueDate, priority) {}
};
class RecurringTask : public Task {
private:
std::string interval;
public:
RecurringTask(const std::string& name, const std::string& interval)
: Task(name, interval, 1), interval(interval) {}
void display() const override {
std::cout << "Recurring Task: " << getName() << ", Interval: " << interval << std::endl;
}
};
#endifGet Life is a SoC’s stories in your inbox
Join Medium for free to get updates from this writer.
Code: TaskManager.h
#ifndef TASKMANAGER_H
#define TASKMANAGER_H
#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
#include <mutex>
#include <thread>
#include <fstream>
#include "Task.h"
#include "TaskProcessor.h"
class TaskManager {
private:
std::vector<std::unique_ptr<Task>> tasks;
std::mutex mtx;
TaskProcessor processor;
public:
void addTask(std::unique_ptr<Task> task) {
std::lock_guard<std::mutex> lock(mtx);
tasks.push_back(std::move(task));
}
void displayTasks() const {
std::lock_guard<std::mutex> lock(mtx);
for (const auto& task : tasks) {
task->display();
}
}
void sortTasksByPriority() {
std::lock_guard<std::mutex> lock(mtx);
std::sort(tasks.begin(), tasks.end(), [](const std::unique_ptr<Task>& a, const std::unique_ptr<Task>& b) {
return a->getPriority() < b->getPriority();
});
}
void completeTask(int index) {
std::lock_guard<std::mutex> lock(mtx);
if (index >= 0 && index < tasks.size()) {
tasks.erase(tasks.begin() + index);
std::cout << "Task completed and removed from the list." << std::endl;
} else {
std::cout << "Invalid task index." << std::endl;
}
}
void processTasks() {
processor.processTasks(tasks);
}
void saveTasks(const std::string& filename) const {
std::lock_guard<std::mutex> lock(mtx);
std::ofstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("Unable to open file.");
}
for (const auto& task : tasks) {
file << task->getName() << "," << task->getDueDate() << "," << task->getPriority() << std::endl;
}
file.close();
}
void loadTasks(const std::string& filename) {
std::lock_guard<std::mutex> lock(mtx);
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("Unable to open file.");
}
tasks.clear();
std::string name, dueDate;
int priority;
while (file >> name >> dueDate >> priority) {
tasks.push_back(std::make_unique<BasicTask>(name, dueDate, priority));
}
file.close();
}
};
#endifCode: TaskProcessor.h
#ifndef TASKPROCESSOR_H
#define TASKPROCESSOR_H
#include <iostream>
#include <thread>
#include <vector>
#include <memory>
#include <mutex>
#include <condition_variable>
#include <queue>
#include "Task.h"
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop;
void worker() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty())
return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
}
public:
ThreadPool(size_t threads) : stop(false) {
for (size_t i = 0; i < threads; ++i)
workers.emplace_back(&ThreadPool::worker, this);
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(mtx);
stop = true;
}
cv.notify_all();
for (std::thread &worker : workers)
worker.join();
}
void enqueueTask(std::function<void()> task) {
{
std::unique_lock<std::mutex> lock(mtx);
tasks.push(std::move(task));
}
cv.notify_one();
}
};
class TaskProcessor {
private:
ThreadPool threadPool;
public:
TaskProcessor(size_t threads = 4) : threadPool(threads) {}
void processTasks(const std::vector<std::unique_ptr<Task>>& tasks) {
for (const auto& task : tasks) {
threadPool.enqueueTask([&task] {
task->display();
});
}
}
};
#endifCode: main.cpp
#include <iostream>
#include "TaskManager.h"
void menu(TaskManager& manager) {
int choice;
while (true) {
std::cout << "\nTask Manager Menu:\n1. Add Task\n2. View Tasks\n3. Sort Tasks by Priority\n4. Complete Task\n5. Process Tasks\n6. Save Tasks\n7. Load Tasks\n8. Exit\nChoose an option: ";
std::cin >> choice;
try {
switch (choice) {
case 1: {
std::string name, dueDate;
int priority;
std::cout << "Enter task name: ";
std::cin.ignore();
std::getline(std::cin, name);
std::cout << "Enter due date: ";
std::getline(std::cin, dueDate);
std::cout << "Enter priority: ";
std::cin >> priority;
manager.addTask(std::make_unique<BasicTask>(name, dueDate, priority));
break;
}
case 2:
manager.displayTasks();
break;
case 3:
manager.sortTasksByPriority();
std::cout << "Tasks sorted by priority." << std::endl;
break;
case 4: {
int index;
std::cout << "Enter task index to complete: ";
std::cin >> index;
manager.completeTask(index);
break;
}
case 5:
std::cout << "Processing tasks concurrently:" << std::endl;
manager.processTasks();
break;
case 6: {
std::string filename;
std::cout << "Enter filename to save tasks: ";
std::cin >> filename;
manager.saveTasks(filename);
std::cout << "Tasks saved to " << filename << std::endl;
break;
}
case 7: {
std::string filename;
std::cout << "Enter filename to load tasks: ";
std::cin >> filename;
manager.loadTasks(filename);
std::cout << "Tasks loaded from " << filename << std::endl;
break;
}
case 8:
std::cout << "Exiting..." << std::endl;
return;
default:
std::cout << "Invalid option." << std::endl;
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
}
}
int main() {
TaskManager manager;
menu(manager);
return 0;
}Explanation
- Task Management: The
TaskManagerclass handles task creation, viewing, sorting, and completion. It also interacts with theTaskProcessorto process tasks concurrently. - Concurrency Management: The
TaskProcessorclass uses aThreadPoolto manage task processing efficiently, leveraging multithreading to improve performance. - File I/O: Tasks can be saved to and loaded from a file, providing persistence for the task data.
- User Interface: A menu-driven command-line interface (CLI) allows users to interact with the Task Management system.
Common Scenarios and Solutions
- Concurrency Issues: Ensure that shared resources are properly synchronized to avoid race conditions and deadlocks.
- Error Handling: Implement robust error handling to manage invalid inputs and runtime exceptions, ensuring the application remains stable.
- Data Persistence: Use file I/O to persist data between sessions, allowing users to save and retrieve their tasks.
Conclusion
This final article in the series demonstrated how to build a complete C++ application that integrates advanced programming concepts such as object-oriented programming, templates, error handling, and multithreading. By following this series, you have learned how to apply these concepts in real-world scenarios, creating robust, efficient, and maintainable applications. The Task Management system we built serves as a practical example of how these techniques can be used together to solve complex problems in software development.







