Build a Task Manager in C++

Build a console task manager in C++ with classes, vectors, and file I/O.

Introduction

30 Second Summary

Everyone has a to-do list. Whether it lives on paper, in an app, or just floats around in your head, keeping track of tasks is something you do every single day.

In this project, you will build a console-based task manager in C++ that lets you add, view, complete, and delete tasks. Your tasks get saved to a file so they persist even after you close the program.

What You'll Build

A fully functional task manager application that runs in your terminal and stores tasks on disk.

By the end of this project, you'll have:

  • A Task struct with properties for title, priority, and completion status.
  • A menu-driven interface using loops and switch statements for user interaction.
  • Data persistence using file I/O so tasks survive program restarts.
  • Secret Mission: Add a priority filter that shows only high-priority incomplete tasks.

Do I need any C++ experience?

Nope. This project starts from zero, including setting up your development environment and compiler. All you need is a Mac and a willingness to learn.

Before We Start

Before We Start...

Set Up Your C++ Development Environment

Before you can build the task manager, you need two things: a compiler that translates your C++ code into a runnable program, and an editor to write that code in.

In this step, you will install the clang++ compiler (via Xcode Command Line Tools), set up VS Code with a C++ extension, and confirm everything works by compiling a simple program.

In this step, get ready to:
  • Install Xcode Command Line Tools to get the clang++ compiler.
  • Install VS Code and the C/C++ extension.
  • Verify your setup by compiling and running a "Hello, World!" program.
Install Xcode Command Line Tools

On macOS, the clang++ compiler comes bundled with Xcode Command Line Tools. This is a free package from Apple that provides essential developer tools without needing the full Xcode app.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check if the compiler is already installed by running this command:
clang --version

✔️ I see Apple clang version 15 or higher

Your compiler is ready to go. The version number confirms you have C++17 support, which is what this project uses.

  • Skip ahead to the next substep (Install VS Code and the C/C++ extension).

ⓧ I see an older version number

An older version of Xcode Command Line Tools is installed, but you need version 15 or higher for full C++17 support.

  • Remove the old Command Line Tools by running:
sudo rm -rf /Library/Developer/CommandLineTools/
  • Your Mac will ask for your password. Type it (nothing will appear on screen) and press Enter.
  • Reinstall the Command Line Tools by running:
xcode-select --install
  • A popup will appear asking you to install the developer tools. Click Install and wait for the download to complete.
  • After installation finishes, verify by running clang --version again. You should now see version 15 or higher.

ⓧ Command not found or a popup appeared

The compiler is not installed yet. If macOS showed a popup asking to install developer tools, click Install in that dialog and wait for it to finish. Otherwise, install manually:

  • Install Xcode Command Line Tools by running:
xcode-select --install
  • A popup will appear. Click Install and accept the license agreement.
  • Wait for the download to finish. This can take a few minutes depending on your internet connection.
  • After installation, verify by running:
clang --version
  • You should see output that includes Apple clang version followed by a version number (15 or higher).

What are Xcode Command Line Tools?

This package includes compilers (clang++), linkers, and other build tools that let you compile C++ code on your Mac. You do not need the full Xcode app for this project.

Seeing 'iostream file not found' after a macOS upgrade?

Sometimes a macOS system update breaks the Command Line Tools. Fix this by removing the old tools and reinstalling:

sudo rm -rf /Library/Developer/CommandLineTools/
xcode-select --install

Help me fix my clang++ installation on macOS.

Install VS Code and the C/C++ extension

VS Code is a lightweight code editor that gives you syntax highlighting, error detection, and autocomplete for C++.

  • Download VS Code from https://code.visualstudio.com/Download. Select the macOS .dmg file for your chip (Apple silicon or Intel).
  • Open the downloaded .dmg file.
  • Drag Visual Studio Code.app to the Applications folder.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.

Now install the C/C++ extension for IntelliSense and debugging support.

  • In VS Code, click the Extensions icon in the left sidebar (it looks like four squares).
  • Type C++ in the search bar at the top of the Extensions panel.
  • Find the extension called C/C++ by Microsoft (it has the highest install count).
  • Click Install.

What does the C/C++ extension do?

This extension (ms-vscode.cpptools) provides IntelliSense (autocomplete suggestions), error highlighting as you type, and integrated debugging. It makes VS Code feel like a full C++ development environment.

Can't find the extension?

Make sure you are searching for C++ (not "C" alone). The extension you want is published by Microsoft and has over 99 million installs.

Help me find and install the C/C++ extension in VS Code.

Verify your setup by compiling a Hello World program

Time to prove everything works. You will create a project folder, write a tiny C++ program, and compile it using clang++.

  • Switch back to your Terminal.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir task-manager
cd task-manager
  • Confirm the folder was created by running:
ls ~/Desktop
  • You should see task-manager in the output.
  • Create your first C++ file by running:
touch task_manager.cpp
  • Open the task-manager folder in VS Code. In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the task-manager folder, and click Open.
  • In the Explorer sidebar on the left, click task_manager.cpp to open it in the editor.
  • Paste the following code into task_manager.cpp:
#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

What does this code do?

#include <iostream> brings in the standard input/output library so you can print text to the terminal.

int main() is the entry point of every C++ program. The code inside the curly braces runs when you execute the compiled program.

std::cout sends text to the console. std::endl adds a newline at the end.

  • Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Open the integrated terminal in VS Code by pressing Ctrl+` (the backtick key, below Escape).
  • Compile your program by running this command:
clang++ -std=c++17 -o task_manager task_manager.cpp

What does this command do?

  • clang++ is the C++ compiler.
  • -std=c++17 tells the compiler to use the C++17 standard.
  • -o task_manager names the output executable task_manager.
  • task_manager.cpp is the source file to compile.
  • Run your compiled program:
./task_manager
  • You should see Hello, World! printed in the terminal.

Don't see Hello, World! in the output?

If you get a "file not found" error, make sure you are in the task-manager folder. Run pwd to check your current directory.

If you see a compilation error, double-check that your code in task_manager.cpp matches the code block above exactly. Watch for missing semicolons or angle brackets.

Help me fix my C++ compilation error.

✔️ Awesome, I've got everything!

Great. Double check you have saved task_manager.cpp and that Hello, World! appeared in the terminal after running ./task_manager.

ⓧ I'd like to double check the full code

Here is the complete task_manager.cpp file at this point:

#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

Your development environment is ready. Next up, you will define the data structures that power the task manager.

Define the Task Structure and Enum

Your development environment is ready and you compiled your first C++ program. Now it is time to define the data that your task manager will work with.

Every task manager needs to know what a "task" actually is. In C++, you define custom data types using structs and enums. In this step, you will create the building blocks that represent a task and its priority level.

In this step, get ready to:
  • Create an enum for task priority levels.
  • Write helper functions to convert between priority types.
  • Build a Task struct with title, priority, and completion fields.
Create an enum for task priority levels

An enum (short for enumeration) defines a fixed set of named values. Instead of using raw numbers like 1, 2, 3 to represent priority, an enum gives those values meaningful names your code can reference directly.

  • In VS Code, open task_manager.cpp from the Explorer sidebar.
  • Select all the existing code by pressing Cmd+A (macOS) or Ctrl+A (Windows).
  • Replace it with the following code that sets up the headers and priority enum:
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <limits>

// Define the three priority levels a task can have
enum class Priority {
    Low,
    Medium,
    High
};

int main() {
    std::cout << "Setup complete!" << std::endl;
    return 0;
}

What does this code do?

The five #include lines bring in standard library features your program needs: console output, file reading/writing, dynamic arrays, string handling, and numeric limits for input validation.

The enum class Priority creates a custom type with exactly three possible values: Low, Medium, and High. The class keyword makes it a scoped enum, meaning you access values with Priority::High instead of just High. This prevents accidental name collisions in larger programs.

  • Save task_manager.cpp.
  • Compile and run your program by running these commands in the terminal:
clang++ -std=c++17 -o task_manager task_manager.cpp
./task_manager
  • You should see Setup complete! printed in the terminal.

Seeing an error about 'iostream' not found?

This can happen after a macOS upgrade. Fix it by removing and reinstalling Xcode Command Line Tools:

sudo rm -rf /Library/Developer/CommandLineTools/
xcode-select --install

Also check that there are no typos in your #include lines. Each header name must be surrounded by angle brackets with no spaces.

Still stuck? Help me fix my clang++ compilation error.

Write a helper function to convert priority to a display string

Your enum stores priority as a type-safe value, but when you display tasks to the user, you need human-readable text like "High" instead of a raw number. A helper function bridges that gap.

  • In task_manager.cpp, find the closing brace of the enum class Priority block (the }; line).
  • Add the following function below it, above int main():
// Convert a Priority value to a readable string for display
std::string priorityToString(Priority p) {
    switch (p) {
        case Priority::Low: return "Low";
        case Priority::Medium: return "Medium";
        case Priority::High: return "High";
        default: return "Unknown";
    }
}

What does this code do?

The priorityToString function takes a Priority value and uses a switch statement to return the matching string. The default case is a safety net that handles any unexpected value.

  • Save task_manager.cpp and compile by running:
clang++ -std=c++17 -o task_manager task_manager.cpp
  • If the command completes with no output, your code compiled successfully.

Seeing a compilation error?

Check that the function is placed after the }; that closes the enum and before int main(). Also make sure you have matching curly braces for the function and the switch block.

Need help? Help me fix my priorityToString compilation error.

You also need a function that works in the opposite direction. When the user types a number (1, 2, or 3) to pick a priority, your code needs to turn that integer into a Priority value.

  • Add the following function directly below priorityToString, still above int main():
// Convert a user-entered integer (1, 2, 3) to a Priority value
Priority intToPriority(int val) {
    switch (val) {
        case 1: return Priority::Low;
        case 2: return Priority::Medium;
        case 3: return Priority::High;
        default: return Priority::Low;
    }
}

What does this code do?

The intToPriority function maps the numbers 1, 2, and 3 to Priority::Low, Priority::Medium, and Priority::High. If the user enters anything else, it defaults to Priority::Low as a safe fallback.

  • Save task_manager.cpp and compile again by running:
clang++ -std=c++17 -o task_manager task_manager.cpp
  • No output means success. Your two helper functions are compiling correctly.
Build the Task struct

A struct groups related pieces of data together under one name. Your Task struct will hold everything you need to know about a single task: its title, its priority level, and whether it has been completed.

  • Add the following struct below the intToPriority function, still above int main():
// A single task with a title, priority level, and completion status
struct Task {
    std::string title;
    Priority priority;
    bool completed;
};

What does this code do?

The Task struct bundles three fields into one custom type. title is a string holding the task description. priority uses the Priority enum you just created. completed is a boolean that tracks whether the task is done.

  • Save task_manager.cpp and compile one more time by running:
clang++ -std=c++17 -o task_manager task_manager.cpp
  • No output means your full file compiles cleanly with all the new types and functions in place.

Getting an error about a missing semicolon?

In C++, struct definitions must end with }; (a semicolon after the closing brace). Make sure the line after your three fields reads exactly }; with no spaces between the brace and semicolon.

Still having trouble? Help me fix my Task struct compilation error.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete task_manager.cpp as it should look at the end of this step:

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <limits>

enum class Priority {
    Low,
    Medium,
    High
};

std::string priorityToString(Priority p) {
    switch (p) {
        case Priority::Low: return "Low";
        case Priority::Medium: return "Medium";
        case Priority::High: return "High";
        default: return "Unknown";
    }
}

Priority intToPriority(int val) {
    switch (val) {
        case 1: return Priority::Low;
        case 2: return Priority::Medium;
        case 3: return Priority::High;
        default: return Priority::Low;
    }
}

struct Task {
    std::string title;
    Priority priority;
    bool completed;
};

int main() {
    std::cout << "Setup complete!" << std::endl;
    return 0;
}

Your data types are defined and compiling cleanly. Next, you will build the TaskManager class that uses these types to add, view, complete, and delete tasks.

Build the Task Manager Class

You have the building blocks in place: an enum for priority levels, a struct to hold task data, and helper functions to convert between types. Now you need something to manage a collection of tasks.

In C++, a class bundles data and the functions that operate on that data into a single unit. In this step, you will create a TaskManager class that holds a list of tasks and provides methods to add, view, complete, and delete them.

In this step, get ready to:
  • Create a TaskManager class that stores tasks in a vector.
  • Implement methods to add, list, complete, and delete tasks.
  • Compile the updated program to confirm the class is valid.
Create the TaskManager class with its first method

A class in C++ uses private members to store internal state and public methods to let outside code interact with that state safely.

  • In task_manager.cpp, find the closing }; of the Task struct.
  • Below it (and above int main()), add the following class definition:
class TaskManager {
private:
    std::vector<Task> tasks;   // stores all tasks in memory
    std::string filename;      // path to the save file

public:
    // constructor initializes the filename
    TaskManager(const std::string& file) : filename(file) {}

    // adds a new task with the given title and priority
    void addTask(const std::string& title, Priority priority) {
        Task task{title, priority, false};
        tasks.push_back(task);
        std::cout << "Task added successfully!\n";
    }
};

What does this code do?

  • std::vector<Task> tasks is a dynamic array that grows as you add tasks. It lives inside the class so only the class methods can modify it directly.
  • The constructor uses an initializer list (: filename(file)) to set the filename when the object is created.
  • addTask() creates a new Task struct with completed set to false, then pushes it onto the vector.
  • Save task_manager.cpp.
  • You won't see a visible change yet. The next chunks add the remaining methods, then you will compile and run.
Add the list and task modification methods

The class needs three more methods: one to display all tasks, one to mark a task complete, and one to remove a task entirely.

  • Inside the TaskManager class, find the closing }; of the class (the one after addTask).
  • Add the listTasks() method just above that closing brace:
    // displays all tasks with their index, status, and priority
    void listTasks() const {
        if (tasks.empty()) {
            std::cout << "No tasks found.\n";
            return;
        }
        std::cout << "\n--- Your Tasks ---\n";
        for (size_t i = 0; i < tasks.size(); ++i) {
            std::cout << i + 1 << ". ";
            std::cout << (tasks[i].completed ? "[X] " : "[ ] ");
            std::cout << tasks[i].title;
            std::cout << " (" << priorityToString(tasks[i].priority) << ")\n";
        }
        std::cout << "------------------\n";
    }

What does this code do?

  • The const keyword after the parentheses means this method promises not to modify any class data. It only reads and prints.
  • The indexed for loop uses size_t (an unsigned integer type) because tasks.size() returns an unsigned value.
  • The ternary operator (? :) prints [X] for completed tasks and [ ] for incomplete ones.
  • Save task_manager.cpp.
  • You won't see a change yet. The compile step is coming after the next chunk.
  • Below listTasks() and still inside the class (above the closing };), add the completeTask() and deleteTask() methods:
    // marks a task as completed by its display number
    void completeTask(int index) {
        if (index < 1 || index > static_cast<int>(tasks.size())) {
            std::cout << "Invalid task number.\n";
            return;
        }
        tasks[index - 1].completed = true;
        std::cout << "Task marked as complete!\n";
    }

    // removes a task from the vector by its display number
    void deleteTask(int index) {
        if (index < 1 || index > static_cast<int>(tasks.size())) {
            std::cout << "Invalid task number.\n";
            return;
        }
        tasks.erase(tasks.begin() + (index - 1));
        std::cout << "Task deleted!\n";
    }

What does this code do?

  • Both methods start with a bounds check. static_cast<int>(tasks.size()) converts the unsigned size to a signed int so the comparison works correctly.
  • completeTask() flips the completed flag to true using index - 1 because vectors are zero-indexed but the user sees tasks starting at 1.
  • deleteTask() uses tasks.erase() with an iterator calculated from tasks.begin(). This removes the element and shifts everything after it down.
  • Save task_manager.cpp.
Compile and test the updated program

The class is fully defined but the program still prints the old message. You will update main() to confirm the class compiles cleanly.

  • In task_manager.cpp, scroll down to the main() function at the bottom of the file.
  • Find this line:
    std::cout << "Setup complete!" << std::endl;
  • Replace "Setup complete!" with "TaskManager class built!", so the line now looks like this:
    std::cout << "TaskManager class built!" << std::endl;
  • Save task_manager.cpp.
  • Compile the program by running this command in your terminal:
clang++ -std=c++17 -o task_manager task_manager.cpp
  • Run the compiled program:
./task_manager
  • You should see TaskManager class built! printed in the terminal. This confirms the entire class compiled without errors.

Seeing a compile error?

  • Check that the class closing }; appears after deleteTask(). A missing semicolon after the closing brace is a common C++ class error.
  • Make sure each method is inside the public: section. If a method accidentally ended up above public:, it will be private and could cause unexpected behavior later.
  • Confirm the class is placed between the Task struct and main(). If it appears after main(), the compiler will not recognize it.

Help me fix my TaskManager class compile error

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete task_manager.cpp at this point:

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <limits>

enum class Priority {
    Low,
    Medium,
    High
};

std::string priorityToString(Priority p) {
    switch (p) {
        case Priority::Low: return "Low";
        case Priority::Medium: return "Medium";
        case Priority::High: return "High";
        default: return "Unknown";
    }
}

Priority intToPriority(int val) {
    switch (val) {
        case 1: return Priority::Low;
        case 2: return Priority::Medium;
        case 3: return Priority::High;
        default: return Priority::Low;
    }
}

struct Task {
    std::string title;
    Priority priority;
    bool completed;
};

class TaskManager {
private:
    std::vector<Task> tasks;
    std::string filename;

public:
    TaskManager(const std::string& file) : filename(file) {}

    void addTask(const std::string& title, Priority priority) {
        Task task{title, priority, false};
        tasks.push_back(task);
        std::cout << "Task added successfully!\n";
    }

    void listTasks() const {
        if (tasks.empty()) {
            std::cout << "No tasks found.\n";
            return;
        }
        std::cout << "\n--- Your Tasks ---\n";
        for (size_t i = 0; i < tasks.size(); ++i) {
            std::cout << i + 1 << ". ";
            std::cout << (tasks[i].completed ? "[X] " : "[ ] ");
            std::cout << tasks[i].title;
            std::cout << " (" << priorityToString(tasks[i].priority) << ")\n";
        }
        std::cout << "------------------\n";
    }

    void completeTask(int index) {
        if (index < 1 || index > static_cast<int>(tasks.size())) {
            std::cout << "Invalid task number.\n";
            return;
        }
        tasks[index - 1].completed = true;
        std::cout << "Task marked as complete!\n";
    }

    void deleteTask(int index) {
        if (index < 1 || index > static_cast<int>(tasks.size())) {
            std::cout << "Invalid task number.\n";
            return;
        }
        tasks.erase(tasks.begin() + (index - 1));
        std::cout << "Task deleted!\n";
    }
};

int main() {
    std::cout << "TaskManager class built!" << std::endl;
    return 0;
}

Your TaskManager class is ready to manage tasks in memory. Next up, you will give it the ability to save tasks to a file and load them back so nothing is lost between sessions.

Add File Persistence

Your task manager can add, view, complete, and delete tasks. But right now, all that work disappears the moment your program closes.

In this step, you'll add file persistence so tasks are written to a file on disk. When the program starts again, it reads that file and restores your task list automatically.

In this step, get ready to:
  • Write a saveToFile() method that writes tasks to disk.
  • Write a loadFromFile() method that reads tasks on startup.
  • Connect persistence to your existing methods so changes are saved automatically.
Write the saveToFile() method

The saveToFile() method opens a file for writing using std::ofstream, then writes each task on its own line. Each field is separated by a pipe character (|) so you can split it back apart later.

  • In task_manager.cpp, find the closing brace of the deleteTask() method.
  • Add the following saveToFile() method directly below it:
    void saveToFile() const {
        // Open the file for writing (overwrites existing content)
        std::ofstream file(filename);
        if (!file.is_open()) {
            std::cout << "Error: Could not save tasks.\n";
            return;
        }
        // Write each task as: title|priority_int|completed_int
        for (const auto& task : tasks) {
            file << task.title << "|"
                 << static_cast<int>(task.priority) << "|"
                 << task.completed << "\n";
        }
        file.close();
    }

What does this code do?

  • std::ofstream file(filename) opens a file for output. If the file doesn't exist, it creates one.
  • The for loop writes each task as a single line in the format title|priority|completed.
  • static_cast<int>(task.priority) converts the enum to a number (0, 1, or 2) so it can be stored as text.

Why use a pipe delimiter?

The pipe character (|) separates each field because task titles might contain commas. Using a comma as a delimiter would break parsing if someone named a task "Buy milk, eggs, bread". This is the same kind of design thinking used in real-world data formats.

  • Save task_manager.cpp.
  • Compile to check for syntax errors by running this command in your terminal:
clang++ -std=c++17 -o task_manager task_manager.cpp

If the code compiles with no errors, you're good. You won't see any output from this method yet because nothing calls it.

Seeing a compile error?

  • Check that the method is inside the TaskManager class (before its closing };), not outside it.
  • Make sure the #include <fstream> line is at the top of your file.

Why won't my saveToFile method compile?

Write the loadFromFile() method

The partner to saveToFile() is loadFromFile(). This method opens the same file for reading, splits each line on the pipe delimiter, and reconstructs Task objects from the saved data.

  • In task_manager.cpp, find the closing brace of the saveToFile() method you just wrote.
  • Add the following loadFromFile() method directly below it:
    void loadFromFile() {
        // Open the file for reading
        std::ifstream file(filename);
        if (!file.is_open()) {
            return;
        }
        tasks.clear();
        std::string line;
        // Read each line and parse it back into a Task
        while (std::getline(file, line)) {
            // Find the two pipe delimiters
            size_t firstDelim = line.find('|');
            size_t secondDelim = line.find('|', firstDelim + 1);
            if (firstDelim == std::string::npos || secondDelim == std::string::npos) {
                continue;
            }
            // Reconstruct the Task from the saved fields
            Task task;
            task.title = line.substr(0, firstDelim);
            task.priority = static_cast<Priority>(
                std::stoi(line.substr(firstDelim + 1, secondDelim - firstDelim - 1))
            );
            task.completed = (line.substr(secondDelim + 1) == "1");
            tasks.push_back(task);
        }
        file.close();
    }

What does this code do?

  • std::ifstream file(filename) opens the file for input (reading). If the file doesn't exist yet, the method returns immediately without error.
  • line.find('|') locates the pipe delimiters. If a line is malformed (missing delimiters), continue skips it safely.
  • line.substr() extracts each field by position. The priority integer is converted back to a Priority enum, and the completed field checks if the value is "1".
  • Save task_manager.cpp.
  • Compile again to check for syntax errors by running:
clang++ -std=c++17 -o task_manager task_manager.cpp

A clean compile means both methods are syntactically correct. The next substep wires them into your existing class.

Getting an undeclared identifier error?

  • Make sure loadFromFile() is placed after saveToFile() inside the class body. Order matters because the compiler reads top to bottom.
  • Verify that #include <string> is at the top of your file. The std::stoi function comes from the string library.

Help me debug my loadFromFile compile error

Connect persistence to your existing methods

Both methods exist, but nothing calls them yet. You need to call loadFromFile() when the program starts and saveToFile() every time a task is changed.

  • In task_manager.cpp, find the constructor at the top of the TaskManager class.
  • Find this line:
    TaskManager(const std::string& file) : filename(file) {}
  • Replace it with this version that calls loadFromFile() on startup:
    TaskManager(const std::string& file) : filename(file) {
        loadFromFile();
    }

Now every time a TaskManager is created, it immediately loads any previously saved tasks from disk.

  • Next, find the addTask() method. Locate this line inside it:
        tasks.push_back(task);
        std::cout << "Task added successfully!\n";
  • Add saveToFile(); between those two lines, so the method now reads:
        tasks.push_back(task);
        saveToFile();
        std::cout << "Task added successfully!\n";
  • Repeat the same pattern for completeTask(). Find this line inside it:
        tasks[index - 1].completed = true;
        std::cout << "Task marked as complete!\n";
  • Add saveToFile(); between those two lines:
        tasks[index - 1].completed = true;
        saveToFile();
        std::cout << "Task marked as complete!\n";
  • Finally, do the same for deleteTask(). Find this line:
        tasks.erase(tasks.begin() + (index - 1));
        std::cout << "Task deleted!\n";
  • Add saveToFile(); between them:
        tasks.erase(tasks.begin() + (index - 1));
        saveToFile();
        std::cout << "Task deleted!\n";
  • Update the main() function at the bottom of the file. Find this line:
    std::cout << "TaskManager class built!" << std::endl;
  • Replace it with:
    std::cout << "File persistence added!" << std::endl;
  • Save task_manager.cpp.
  • Compile and run your program by running these commands:
clang++ -std=c++17 -o task_manager task_manager.cpp
./task_manager

You should see File persistence added! printed in your terminal. This confirms that everything compiles and the constructor's loadFromFile() call ran without crashing (even though no file exists yet).

Seeing an error about saveToFile not being declared?

  • The compiler reads your file top to bottom. Make sure saveToFile() appears in the class BEFORE loadFromFile(), and both appear AFTER deleteTask().
  • The constructor calls loadFromFile() at the top of the class, but in C++ a class can call any of its own methods regardless of declaration order. The issue is usually that the method was accidentally placed outside the class braces.

Help me fix my compile error with saveToFile or loadFromFile

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete task_manager.cpp at this point:

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <limits>

enum class Priority {
    Low,
    Medium,
    High
};

std::string priorityToString(Priority p) {
    switch (p) {
        case Priority::Low: return "Low";
        case Priority::Medium: return "Medium";
        case Priority::High: return "High";
        default: return "Unknown";
    }
}

Priority intToPriority(int val) {
    switch (val) {
        case 1: return Priority::Low;
        case 2: return Priority::Medium;
        case 3: return Priority::High;
        default: return Priority::Low;
    }
}

struct Task {
    std::string title;
    Priority priority;
    bool completed;
};

class TaskManager {
private:
    std::vector<Task> tasks;
    std::string filename;

public:
    TaskManager(const std::string& file) : filename(file) {
        loadFromFile();
    }

    void addTask(const std::string& title, Priority priority) {
        Task task{title, priority, false};
        tasks.push_back(task);
        saveToFile();
        std::cout << "Task added successfully!\n";
    }

    void listTasks() const {
        if (tasks.empty()) {
            std::cout << "No tasks found.\n";
            return;
        }
        std::cout << "\n--- Your Tasks ---\n";
        for (size_t i = 0; i < tasks.size(); ++i) {
            std::cout << i + 1 << ". ";
            std::cout << (tasks[i].completed ? "[X] " : "[ ] ");
            std::cout << tasks[i].title;
            std::cout << " (" << priorityToString(tasks[i].priority) << ")\n";
        }
        std::cout << "------------------\n";
    }

    void completeTask(int index) {
        if (index < 1 || index > static_cast<int>(tasks.size())) {
            std::cout << "Invalid task number.\n";
            return;
        }
        tasks[index - 1].completed = true;
        saveToFile();
        std::cout << "Task marked as complete!\n";
    }

    void deleteTask(int index) {
        if (index < 1 || index > static_cast<int>(tasks.size())) {
            std::cout << "Invalid task number.\n";
            return;
        }
        tasks.erase(tasks.begin() + (index - 1));
        saveToFile();
        std::cout << "Task deleted!\n";
    }

    void saveToFile() const {
        std::ofstream file(filename);
        if (!file.is_open()) {
            std::cout << "Error: Could not save tasks.\n";
            return;
        }
        for (const auto& task : tasks) {
            file << task.title << "|"
                 << static_cast<int>(task.priority) << "|"
                 << task.completed << "\n";
        }
        file.close();
    }

    void loadFromFile() {
        std::ifstream file(filename);
        if (!file.is_open()) {
            return;
        }
        tasks.clear();
        std::string line;
        while (std::getline(file, line)) {
            size_t firstDelim = line.find('|');
            size_t secondDelim = line.find('|', firstDelim + 1);
            if (firstDelim == std::string::npos || secondDelim == std::string::npos) {
                continue;
            }
            Task task;
            task.title = line.substr(0, firstDelim);
            task.priority = static_cast<Priority>(
                std::stoi(line.substr(firstDelim + 1, secondDelim - firstDelim - 1))
            );
            task.completed = (line.substr(secondDelim + 1) == "1");
            tasks.push_back(task);
        }
        file.close();
    }
};

int main() {
    std::cout << "File persistence added!" << std::endl;
    return 0;
}

Your task manager now has a memory. Tasks written to tasks.txt will survive program restarts and be loaded right back in. Next up, you'll build the interactive menu that lets you actually use all these features from the command line.

Create the Interactive Menu

Your TaskManager class can add, view, complete, delete, save, and load tasks. All of that power is locked away behind method names that only code can call.

In this step, you will build a menu-driven interface that lets a real person interact with the task manager from the terminal. A while loop keeps the program running until the user chooses to quit, and a switch statement routes each menu choice to the right method.

In this step, get ready to:
  • Build a menu loop using while and switch statements.
  • Handle user input with std::cin and input validation.
  • Connect menu choices to TaskManager methods and auto-save after changes.
Build the menu loop with while and switch

The menu loop is the backbone of any console application. It prints options, waits for input, dispatches the choice, then repeats.

  • In task_manager.cpp, find the placeholder main() function at the bottom of the file.
  • Delete the entire main() function (from int main() { through its closing }).
  • Replace it with this menu loop:
int main() {
    // Create the task manager, loading any existing tasks from file
    TaskManager manager("tasks.txt");

    int choice = 0;
    while (true) {
        // Display the menu options
        std::cout << "\n=== Task Manager ===\n";
        std::cout << "1. Add Task\n";
        std::cout << "2. View Tasks\n";
        std::cout << "3. Complete Task\n";
        std::cout << "4. Delete Task\n";
        std::cout << "5. Quit\n";
        std::cout << "Choose an option: ";
        std::cin >> choice;

        // Handle non-numeric input
        if (std::cin.fail()) {
            std::cin.clear();
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            std::cout << "Please enter a number.\n";
            continue;
        }
        // Clear the leftover newline from the input buffer
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

        switch (choice) {
            case 2:
                manager.listTasks();
                break;
            case 5:
                std::cout << "Goodbye!\n";
                return 0;
            default:
                std::cout << "Invalid option. Try again.\n";
                break;
        }
    }
    return 0;
}

What does this code do?

TaskManager manager("tasks.txt") creates a manager that immediately loads any previously saved tasks from disk.

The while (true) loop prints the menu and reads a number into choice every iteration. If the user types something that is not a number, std::cin.fail() catches it, clears the error state, and discards the bad input so the loop can try again.

The std::cin.ignore() after a successful read removes the leftover newline character. Without it, the next std::getline call would read an empty string instead of waiting for real input.

  • Save task_manager.cpp.
  • Compile and run by entering these commands in your terminal:
clang++ -std=c++17 -o task_manager task_manager.cpp
./task_manager

You should see the menu printed. Type 2 and press Enter to view tasks (you will see "No tasks found."). Then type 5 and press Enter to quit.

Menu not appearing?

  • Make sure you deleted the entire old main() function before pasting the new one. Two main() functions will cause a compilation error.
  • Check that the opening { after int main() and the final closing } are both present. A missing brace causes a cascade of compiler errors.
  • If the program compiles but hangs without printing anything, check that you have \n at the end of each std::cout line.

Still stuck? Help me debug my menu loop compilation error.

Handle the Add Task input

Adding a task is the most complex menu option because it collects two pieces of input: a title (a full line of text) and a priority number. Each needs its own validation.

  • In task_manager.cpp, find case 2: inside the switch statement.
  • Add the following case 1: block directly above case 2::
            case 1: {
                std::string title;
                std::cout << "Enter task title: ";
                std::getline(std::cin, title);
                // Reject empty titles
                if (title.empty()) {
                    std::cout << "Title cannot be empty.\n";
                    break;
                }
                std::cout << "Priority (1=Low, 2=Medium, 3=High): ";
                int pVal;
                std::cin >> pVal;
                // Default to Low if priority input is invalid
                if (std::cin.fail() || pVal < 1 || pVal > 3) {
                    std::cin.clear();
                    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                    std::cout << "Invalid priority. Using Low.\n";
                    pVal = 1;
                }
                std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                manager.addTask(title, intToPriority(pVal));
                break;
            }

What does this code do?

std::getline(std::cin, title) reads an entire line (spaces included) into title. This works because the earlier cin.ignore() already cleared the newline left behind by the menu choice read.

The priority validation checks two conditions: whether the input failed entirely (non-numeric) and whether the number is outside the 1-3 range. If either is true, it falls back to Low rather than crashing.

The curly braces around case 1: create a local scope. Without them, declaring title and pVal inside the case would cause a compiler error.

  • Save task_manager.cpp.
  • Compile and run:
clang++ -std=c++17 -o task_manager task_manager.cpp
./task_manager

Choose option 1, type a title like Buy groceries, and enter 2 for Medium priority. You should see "Task added successfully!" printed.

Title prompt getting skipped?

  • If the program prints "Enter task title:" and immediately says "Title cannot be empty," the input buffer still has a leftover newline. Make sure the std::cin.ignore() line appears right after the successful std::cin >> choice read (before the switch statement).
  • Check that the case 1: block is wrapped in curly braces { }. Missing braces will cause a variable declaration error.

Need help? Why is std::getline reading an empty string instead of waiting for my input?

Connect the Complete and Delete options

The remaining two options follow the same pattern: show the current task list, ask for a task number, validate the input, then call the appropriate method.

  • In task_manager.cpp, find case 5: inside the switch statement.
  • Add the following two cases directly above case 5::
            case 3: {
                manager.listTasks();
                std::cout << "Enter task number to complete: ";
                int num;
                std::cin >> num;
                if (std::cin.fail()) {
                    std::cin.clear();
                    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                    std::cout << "Invalid input.\n";
                    break;
                }
                std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                manager.completeTask(num);
                break;
            }
            case 4: {
                manager.listTasks();
                std::cout << "Enter task number to delete: ";
                int num;
                std::cin >> num;
                if (std::cin.fail()) {
                    std::cin.clear();
                    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                    std::cout << "Invalid input.\n";
                    break;
                }
                std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                manager.deleteTask(num);
                break;
            }

What does this code do?

Both cases start with manager.listTasks() so the user can see the numbered list before picking which task to act on.

The input validation pattern is the same one you used for the menu choice: check std::cin.fail(), clear the error, and discard the bad input. The completeTask() and deleteTask() methods handle the bounds-checking internally, printing "Invalid task number." if the index is out of range.

  • Save task_manager.cpp.
  • Compile and run the full program:
clang++ -std=c++17 -o task_manager task_manager.cpp
./task_manager
  • Test the full workflow: add a task (option 1), view tasks (option 2), mark it complete (option 3), view again to see the [X] marker, then delete it (option 4).
  • Quit with option 5, then run ./task_manager again. Choose option 2 to confirm your tasks survived the restart.

Tasks disappearing after restart?

  • Check that you are running ./task_manager from the same directory where tasks.txt was created. The file path is relative, so changing directories means the program looks for a different file.
  • If you see "Invalid task number" when completing or deleting, remember the number shown next to the task in the list (not the priority number).

Still having issues? Help me debug why my tasks are not persisting between runs.

✔️ Awesome, I've got everything!

Great. Double check you have saved task_manager.cpp and that it compiles without errors.

ⓧ I'd like to double check the full code

Here is the complete task_manager.cpp file. Compare yours against it:

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <limits>

enum class Priority {
    Low,
    Medium,
    High
};

std::string priorityToString(Priority p) {
    switch (p) {
        case Priority::Low: return "Low";
        case Priority::Medium: return "Medium";
        case Priority::High: return "High";
        default: return "Unknown";
    }
}

Priority intToPriority(int val) {
    switch (val) {
        case 1: return Priority::Low;
        case 2: return Priority::Medium;
        case 3: return Priority::High;
        default: return Priority::Low;
    }
}

struct Task {
    std::string title;
    Priority priority;
    bool completed;
};

class TaskManager {
private:
    std::vector<Task> tasks;
    std::string filename;

public:
    TaskManager(const std::string& file) : filename(file) {
        loadFromFile();
    }

    void addTask(const std::string& title, Priority priority) {
        Task task{title, priority, false};
        tasks.push_back(task);
        saveToFile();
        std::cout << "Task added successfully!\n";
    }

    void listTasks() const {
        if (tasks.empty()) {
            std::cout << "No tasks found.\n";
            return;
        }
        std::cout << "\n--- Your Tasks ---\n";
        for (size_t i = 0; i < tasks.size(); ++i) {
            std::cout << i + 1 << ". ";
            std::cout << (tasks[i].completed ? "[X] " : "[ ] ");
            std::cout << tasks[i].title;
            std::cout << " (" << priorityToString(tasks[i].priority) << ")\n";
        }
        std::cout << "------------------\n";
    }

    void completeTask(int index) {
        if (index < 1 || index > static_cast<int>(tasks.size())) {
            std::cout << "Invalid task number.\n";
            return;
        }
        tasks[index - 1].completed = true;
        saveToFile();
        std::cout << "Task marked as complete!\n";
    }

    void deleteTask(int index) {
        if (index < 1 || index > static_cast<int>(tasks.size())) {
            std::cout << "Invalid task number.\n";
            return;
        }
        tasks.erase(tasks.begin() + (index - 1));
        saveToFile();
        std::cout << "Task deleted!\n";
    }

    void saveToFile() const {
        std::ofstream file(filename);
        if (!file.is_open()) {
            std::cout << "Error: Could not save tasks.\n";
            return;
        }
        for (const auto& task : tasks) {
            file << task.title << "|"
                 << static_cast<int>(task.priority) << "|"
                 << task.completed << "\n";
        }
        file.close();
    }

    void loadFromFile() {
        std::ifstream file(filename);
        if (!file.is_open()) {
            return;
        }
        tasks.clear();
        std::string line;
        while (std::getline(file, line)) {
            size_t firstDelim = line.find('|');
            size_t secondDelim = line.find('|', firstDelim + 1);
            if (firstDelim == std::string::npos || secondDelim == std::string::npos) {
                continue;
            }
            Task task;
            task.title = line.substr(0, firstDelim);
            task.priority = static_cast<Priority>(
                std::stoi(line.substr(firstDelim + 1, secondDelim - firstDelim - 1))
            );
            task.completed = (line.substr(secondDelim + 1) == "1");
            tasks.push_back(task);
        }
        file.close();
    }
};

int main() {
    TaskManager manager("tasks.txt");

    int choice = 0;
    while (true) {
        std::cout << "\n=== Task Manager ===\n";
        std::cout << "1. Add Task\n";
        std::cout << "2. View Tasks\n";
        std::cout << "3. Complete Task\n";
        std::cout << "4. Delete Task\n";
        std::cout << "5. Quit\n";
        std::cout << "Choose an option: ";
        std::cin >> choice;

        if (std::cin.fail()) {
            std::cin.clear();
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            std::cout << "Please enter a number.\n";
            continue;
        }
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

        switch (choice) {
            case 1: {
                std::string title;
                std::cout << "Enter task title: ";
                std::getline(std::cin, title);
                if (title.empty()) {
                    std::cout << "Title cannot be empty.\n";
                    break;
                }
                std::cout << "Priority (1=Low, 2=Medium, 3=High): ";
                int pVal;
                std::cin >> pVal;
                if (std::cin.fail() || pVal < 1 || pVal > 3) {
                    std::cin.clear();
                    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                    std::cout << "Invalid priority. Using Low.\n";
                    pVal = 1;
                }
                std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                manager.addTask(title, intToPriority(pVal));
                break;
            }
            case 2:
                manager.listTasks();
                break;
            case 3: {
                manager.listTasks();
                std::cout << "Enter task number to complete: ";
                int num;
                std::cin >> num;
                if (std::cin.fail()) {
                    std::cin.clear();
                    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                    std::cout << "Invalid input.\n";
                    break;
                }
                std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                manager.completeTask(num);
                break;
            }
            case 4: {
                manager.listTasks();
                std::cout << "Enter task number to delete: ";
                int num;
                std::cin >> num;
                if (std::cin.fail()) {
                    std::cin.clear();
                    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                    std::cout << "Invalid input.\n";
                    break;
                }
                std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                manager.deleteTask(num);
                break;
            }
            case 5:
                std::cout << "Goodbye!\n";
                return 0;
            default:
                std::cout << "Invalid option. Try again.\n";
                break;
        }
    }
    return 0;
}

Secret mission

Priority Filter

Your task manager works great, but when your list grows long, finding urgent tasks gets harder. Add a priority filter that instantly shows only incomplete high-priority tasks so you can focus on what matters most.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your files for future experimentation or delete them entirely.

Resources you used:

  • Your project folder on the Desktop (containing task_manager.cpp, the compiled binary, and tasks.txt).
  • VS Code with the C/C++ extension.

Keep everything running

No action needed. Choose this if you want to keep building on your task manager or experimenting with C++.

  • Your project files stay on your Desktop, ready for you to compile and run anytime.
  • VS Code and the C/C++ extension remain installed for future C++ projects.
  • Xcode Command Line Tools stay installed on your system (other developer tools depend on them).

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your files sit quietly on disk and consume no CPU or memory.

  • Close VS Code to free up memory.
  • Your project folder and tasks.txt remain on your Desktop whenever you are ready to return.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes your source code, compiled binary, and saved tasks permanently.

  • Delete your project folder by running this command in Terminal:
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]
  • To remove VS Code, open Finder, go to Applications, and drag Visual Studio Code to the Trash.
  • If you want to keep VS Code but remove the C/C++ extension, open VS Code, click the Extensions icon in the left sidebar, find C/C++, and click Uninstall.

Nice Work!

Nice Work!

You just built a real C++ application from scratch. Your task manager can add, view, complete, and delete tasks, and it saves everything to disk so nothing is lost between sessions.

You've learned how to:

  • Build a complete task manager using classes, vectors, and enums to organize your code into clean, reusable structures.
  • Implement file persistence with std::ofstream and std::ifstream so your data survives program restarts.
  • Set up a working C++ development environment with clang++ and VS Code, ready for future projects.
  • Added a priority filter that finds only incomplete high-priority tasks so you can focus on what matters most.

Ready to quiz yourself?