Build a Persistent C++ Task Tracker
Build a C++ terminal task tracker that saves tasks between sessions.
Introduction
30 Second Summary
Task lists are only useful when they remember what you entered. A list that empties every time it closes quickly becomes one more thing to manage.
In this project, you will build a persistent C++ task tracker that runs in macOS Terminal. Its menu keeps your saved tasks ready every time you return.
What You'll Build
You will demo a terminal tracker that restores the same completed task after you quit and reopen it.
By the end of this project, you'll have:
- A working task menu where you can add full task descriptions. You can list each entry with a visible pending or completed status.
- Safe task completion using displayed numbers. Bad input produces a helpful message before the menu continues.
- Restart proof that saved descriptions and completion states return from tasks.txt.
- Secret Mission: Add a progress option that reports completed tasks out of the total.
Are there any prerequisites?
You need a Mac with Visual Studio Code installed plus permission to install Apple's free Command Line Tools for Xcode. No prior C++ experience is required.
Before We Start
You are building a terminal task tracker that adds, lists, completes, and restores tasks after a restart. That goal gives every C++ concept a visible purpose before the hands-on work begins.
Set Up and Prove the C++ Toolchain
The task tracker starts as C++ source code. Your Mac needs a compiler to turn that source into a program it can run.
Visual Studio Code gives you the editor for main.cpp.
Command Line Tools for Xcode supplies Apple Clang. Apple Clang handles compiling and linking.
In this step, get ready to:
- Install Apple Command Line Tools.
- Create the cpp-task-tracker starter project.
- Prove the toolchain with a running executable.
Why Apple Clang Instead of CMake?
This project has one source file. Apple Clang can compile it directly.
CMake becomes useful when a project has multiple source files or libraries. Adding it here would hide the beginner workflow behind extra setup.
Prepare your Mac's toolchain
The Command Line Tools package installs Apple's command-line toolchain. It gives Terminal the compiler used in this project.
- Press Cmd+Space to open Spotlight.
- Type Terminal into Spotlight.
- Press Enter to open macOS Terminal.
- Start Apple's installer by running this command:
xcode-select --install
What Does This Command Do?
The command opens Apple's installer flow for Command Line Tools.
The installed package provides the macOS SDK. It also provides the toolchain binaries used for command-line builds.
- Complete Apple's installer flow.
The installation can take a few minutes. A quiet progress bar during that wait is normal.
- Inspect the installed package by running this command:
pkgutil --pkg-info=com.apple.pkg.CLTools_Executables
What Does This Check Prove?
The command reads the installed Command Line Tools package information. The output includes the package version selected for your version of macOS.
You'll see information for the installed package. That is the system-level setup done.
Your Mac now has the tools needed to build the task tracker.
Package Details Missing?
- Let the installer finish before checking the package.
- Run the package inspection command above again after the installer closes.
- Restart Terminal if the package still does not appear.
Help me verify why Apple Command Line Tools package details are missing.
Set up the starter workspace
A workspace gives the editor and integrated terminal the same folder as their starting point. This keeps the source file beside the executable you compile.
- Press Cmd+Space to open Spotlight.
- Type Visual Studio Code into Spotlight.
- Press Enter to open Visual Studio Code.
You'll see the Visual Studio Code window. Opening a new folder may trigger a folder trust prompt.
- Click File in the menu bar.
- Select Open Folder....
- Select Desktop in the file picker.
- Select New Folder.
- Enter cpp-task-tracker as the folder name.
- Select Open.
You'll see cpp-task-tracker at the top of the Explorer view.
- Confirm the folder trust prompt if Visual Studio Code shows one.
- Select the New File... button in the Explorer view.
- Enter main.cpp as the file name.
You'll see a blank main.cpp editor tab inside the cpp-task-tracker workspace.
- Create the starter program by pasting this code into main.cpp:
#include <iostream>
int main()
{
std::cout << "C++ setup works\n";
return 0;
}
What Does This Code Do?
- The #include <iostream> line makes console output available.
- The int main() function is where the program starts.
- The std::cout statement writes C++ setup works to the terminal.
- The return 0 statement reports that the program finished successfully.
- Save main.cpp by pressing Cmd+S.
- Open the integrated terminal by pressing Control+`.
You'll see a terminal panel at the bottom of Visual Studio Code. It starts inside the open cpp-task-tracker workspace.
- Confirm the source file exists by running this command:
ls
What Does This Command Do?
The ls command lists the files in the terminal's current folder. It confirms that the integrated terminal points at the same workspace shown in the Explorer.
You'll see main.cpp in the output.
Cannot See main.cpp?
- Confirm that main.cpp appears under cpp-task-tracker in the Explorer.
- Save main.cpp before running the file check again.
- Reopen the integrated terminal after the correct folder appears in the Explorer.
Help me find main.cpp from the Visual Studio Code integrated terminal.
Prove the toolchain end to end
Compilation turns main.cpp into machine code. Linking packages that code with the C++ runtime to create an executable.
The clang++ driver handles both jobs for this single-file app. The warning flags help catch mistakes while you learn.
- Check Apple Clang by running this command:
clang --version
What Does This Check Prove?
The command asks the installed compiler to report its version information. A version response confirms that Terminal can find Apple Clang.
You'll see Apple Clang version information in the integrated terminal.
Clang Not Available?
- Confirm that the Command Line Tools installer completed.
- Restart Visual Studio Code after the installation.
- Run the package inspection command from the first substep again.
Help me make Apple Clang available in the Visual Studio Code terminal.
Before you run this, what line do you expect the executable to print?
- Compile main.cpp and launch the executable by running this command:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker && ./task-tracker
What Does This Command Do?
- The clang++ driver compiles the C++ source and links the C++ runtime.
- The -std=c++17 option selects the C++17 language standard.
- The -Wall option enables common compiler warnings.
- The -Wextra option enables additional warning groups.
- The -o task-tracker option names the compiled executable task-tracker.
- The && ./task-tracker part starts the executable after compilation succeeds.
You'll see C++ setup works in the terminal. Your Mac has now compiled and run its first piece of this project.
Starter Program Not Running?
- Confirm that the integrated terminal belongs to the cpp-task-tracker workspace.
- Save main.cpp before compiling again.
- Compare your source file with the complete reference below.
Help me diagnose why the C++ starter program does not compile or run.
✔️ Awesome, I've got everything!
Great. Your saved main.cpp now compiles into the runnable task-tracker executable.
Running the executable prints C++ setup works.
ⓧ I'd like to double check the full code
Compare your saved main.cpp with this complete file:
#include <iostream>
int main()
{
std::cout << "C++ setup works\n";
return 0;
}
What Should Match?
This reference is the complete source file for this step. Matching every line preserves the exact program that produced your successful terminal output.
Your Mac can now turn C++ source code into a running executable. Next up, you'll replace the setup message with the task tracker's menu.
Build the App Menu
Your toolchain can already turn a C++ source file into an executable. The current program proves the compiler works.
A visible menu creates the smallest useful version of the tracker. Each choice gives a later feature a clear place to connect.
In this step, get ready to:
- Replace the setup message with the tracker menu.
- Rebuild the executable to display four choices.
- Trace each visible line back to its source code.
Replace the setup message with a menu
The existing std::cout statement sends one message to the terminal. You can chain several output strings together to build the complete menu.
- Return to the main.cpp file from earlier in Visual Studio Code.
- Replace the existing contents of main.cpp by pasting this code:
#include <iostream>
int main()
{
std::cout << "\nC++ Task Tracker\n"
<< "1. Add task\n"
<< "2. List tasks\n"
<< "3. Complete task\n"
<< "4. Quit\n";
return 0;
}
What does this code do?
- The std::cout statement sends the tracker title to the terminal.
- Each << operator adds another string to the same output statement.
- Each \n moves the next piece of text onto a new line.
- The numbered string literals define the four menu choices in their displayed order.
- Save main.cpp.
Before you rebuild, how many numbered choices do you expect the new executable to display?
- Rebuild the executable and launch it in the integrated terminal by running:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker && ./task-tracker
What does this command do?
- The clang++ driver compiles the C++ source file.
- The -std=c++17 option selects the C++17 language standard.
- The -Wall option enables a useful group of compiler warnings.
- The -Wextra option enables additional compiler warnings.
- The -o option names the rebuilt executable task-tracker.
- The ./task-tracker command launches that executable after compilation succeeds.
You'll see C++ Task Tracker on the first visible line. Below it, you'll see Add task, List tasks, Complete task, and Quit.
That is your first visible app screen working from source code through to a rebuilt executable.
Menu missing or unchanged?
- Make sure you saved main.cpp before rebuilding the executable.
- Check that the integrated terminal is still inside the cpp-task-tracker folder.
- Compare every quote and bracket in main.cpp with the code above.
Help me diagnose why my rebuilt task tracker does not display the four menu choices.
✔️ Awesome, I've got everything!
Great. Your saved main.cpp should match the menu that just ran.
ⓧ I'd like to double check the full code
Compare your complete main.cpp file with this version.
#include <iostream>
int main()
{
std::cout << "\nC++ Task Tracker\n"
<< "1. Add task\n"
<< "2. List tasks\n"
<< "3. Complete task\n"
<< "4. Quit\n";
return 0;
}
Trace the menu output
Every visible menu line comes from a string literal inside the same output statement. Tracing those pairs helps you connect source code with terminal output.
- Find the "\nC++ Task Tracker\n" string in main.cpp.
- Match each numbered string literal in main.cpp to its visible menu choice.
- Check that the string literals appear in the same order as the terminal menu.
How does the output map to the code?
- The first \n places a blank line before the tracker title.
- Each numbered string literal produces one numbered choice.
- The final \n after Quit leaves the terminal prompt on a fresh line.
- The semicolon closes the complete std::cout statement after all five strings are connected.
Before you run the executable again, can you predict the exact order of the title and four choices?
- Confirm the rebuilt menu one final time by running:
./task-tracker
What does this final run prove?
The ./task-tracker command launches the executable you just rebuilt. Its output confirms that the compiled file matches your current source.
You'll see C++ Task Tracker first. You'll then see all four numbered choices in order.
Still seeing the setup message?
- Save the menu version of main.cpp.
- Run the full compile command from the previous substep again.
- Confirm that the terminal is launching the task-tracker executable inside the cpp-task-tracker folder.
Help me find why the old setup message still appears after rebuilding the executable.
Your tracker now has a stable menu that users can read. Next up, you'll connect its choices to tasks stored in memory.
Add and List Tasks
Your compiled menu now gives the task tracker a clear shape.
Now the Add and List choices need working behavior. A C++ vector keeps the current tasks together while the program runs.
What happens to that vector when the executable stops? You will answer that question by restarting the tracker after adding a task.
In this step, get ready to:
- Define a task data model with descriptions and completion states.
- Add multi-word task descriptions and list their status markers.
- Restart the tracker to test the limits of in-memory storage.
Define the task data model
A struct groups related values into one record. Each Task stores a description beside its completion state.
The TaskList alias gives the vector a shorter project-specific name. Every helper can use that same name for the collection of tasks.
- In main.cpp, replace the existing include line with this data model:
#include <iostream>
#include <string>
#include <vector>
struct Task {
std::string description;
bool completed = false;
};
using TaskList = std::vector<Task>;
What does this code do?
- The <string> header provides text values for task descriptions.
- The <vector> header provides a collection that can grow as you add tasks.
- The Task struct stores each description beside its completion state.
- The TaskList alias represents a vector filled with Task records.
- Save main.cpp.
- Confirm the new data model compiles by rebuilding and running the tracker with this command:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker && ./task-tracker
What does this check prove?
- The clang++ command rebuilds task-tracker from your updated source file.
- The final part launches the rebuilt executable after compilation succeeds.
Good. You will see the tracker title and its four choices again. That output proves the new data model compiles without disrupting the existing menu.
Seeing a compiler error?
- Check that the three include lines appear at the top of main.cpp.
- Check that Task closes with a brace followed by a semicolon.
- Check that TaskList uses the same capitalization everywhere.
Help me diagnose the compiler error in my task data model.
Create the task helpers
The listTasks() function turns stored records into readable terminal output. A range-based loop visits every task in the collection.
- Place this function below the TaskList alias in main.cpp:
void listTasks(const TaskList& tasks)
{
std::cout << "\nTasks\n";
if (tasks.size() == 0) {
std::cout << "No tasks yet.\n";
return;
}
for (const Task& task : tasks) {
std::cout << (task.completed ? "[x] " : "[ ] ")
<< task.description << '\n';
}
}
How does listing work?
- The const TaskList& tasks parameter lets the function read the collection without copying or changing it.
- The empty-list condition prints No tasks yet. before leaving the function.
- The loop selects [x] for a completed task.
- The loop selects [ ] for a pending task.
- Save main.cpp.
- Confirm the listing helper compiles by rebuilding and running the tracker:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker && ./task-tracker
What did this build check?
This rebuild checks the function signature and the loop syntax. The menu output confirms the resulting executable still runs.
Your listing helper now compiles as part of the tracker. The four menu choices still appear when the executable runs.
Is the listing helper failing to compile?
- Check that listTasks() appears above main().
- Check the braces around the empty-list condition.
- Check the parentheses around the two status markers.
Help me find the syntax problem in my listTasks function.
The addTask() function collects one complete line of text. Using std::getline allows a description such as Learn C++ loops to keep every word.
- Place this function immediately below listTasks() in main.cpp:
void addTask(TaskList& tasks)
{
std::cout << "Task description: ";
std::string description;
std::getline(std::cin, description);
tasks.push_back({description, false});
std::cout << "Task added.\n";
}
How does adding work?
- The TaskList& tasks parameter gives the function access to the original collection.
- The description variable holds the complete line entered in the terminal.
- The push_back() call adds a new pending task to the end of the vector.
- The false value gives every new task a pending state.
- Save main.cpp.
- Confirm both helper functions compile by rebuilding and running the tracker:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker && ./task-tracker
What does this build confirm?
The successful build confirms that both helpers accept the TaskList type correctly. The rebuilt executable remains ready for the menu loop.
You now have the two jobs that power the Add and List choices. The next change connects those helpers to the menu.
Is the add helper failing to compile?
- Check that addTask() accepts TaskList& tasks.
- Check that std::getline receives std::cin first.
- Check that the new task contains the description followed by false.
Help me troubleshoot my addTask function.
Connect the running menu
The current main() prints the menu once before ending. A loop keeps the process alive for repeated choices.
String comparisons route Add and List to the helpers you just created. The Quit choice changes the loop condition so the executable can finish cleanly.
- In main.cpp, replace the existing main() function with this version:
int main()
{
TaskList tasks;
bool running = true;
while (running) {
std::cout << "\nC++ Task Tracker\n"
<< "1. Add task\n"
<< "2. List tasks\n"
<< "3. Complete task\n"
<< "4. Quit\n";
std::string choice;
std::getline(std::cin, choice);
if (choice == "1") {
addTask(tasks);
} else if (choice == "2") {
listTasks(tasks);
} else if (choice == "4") {
running = false;
std::cout << "Goodbye.\n";
}
}
return 0;
}
How does the menu stay active?
- The tasks variable holds every task added during the current run.
- The running variable controls how long the menu loop continues.
- The choice variable captures one complete menu entry.
- The conditions call the matching helper for choices 1 and 2.
- Save main.cpp.
Before you run this version, which earlier one-time menu behavior do you expect the loop to change?
- Rebuild and launch the interactive tracker with this command:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker && ./task-tracker
What does this command do?
The command compiles the updated source before launching the new executable. The terminal remains attached to the tracker while its menu loop is running.
You will see the four choices. The tracker now waits below the menu for your first selection.
- Enter 1 at the menu.
- Enter Learn C++ loops after Task description:.
- Enter 2 at the menu.
That is your first working task flow. You will see [ ] Learn C++ loops below the Tasks heading.
- Enter 4 at the menu.
You will see Goodbye. before the process returns control to the terminal.
Before you restart the executable, do you think the new process can recover the task you just added?
- Start the executable again by running:
./task-tracker
What is this restart testing?
This command launches a fresh tracker process after the earlier process has ended. The new run begins with the values created inside its own main() function.
- Enter 2 at the restarted menu.
You will see No tasks yet. The task disappeared because the vector existed only in the earlier running process.
You found the exact boundary of in-memory storage. The tracker works during one run, but a restart begins with an empty TaskList.
- Enter 4 to stop the restarted tracker.
Does the menu close or skip words?
- Check that the menu and choice conditions remain inside the while (running) block if the executable closes after one choice.
- Check that addTask() uses std::getline if the description loses words after a space.
- Check that choices 1 and 2 call the matching helper functions.
Help me debug my interactive Add and List menu.
✔️ Awesome, I've got everything!
Great. Double check that main.cpp is saved. Your tracker can now add and list tasks throughout one running session.
ⓧ I'd like to double check the full code
Compare your complete main.cpp file with this version:
#include <iostream>
#include <string>
#include <vector>
struct Task {
std::string description;
bool completed = false;
};
using TaskList = std::vector<Task>;
void listTasks(const TaskList& tasks)
{
std::cout << "\nTasks\n";
if (tasks.size() == 0) {
std::cout << "No tasks yet.\n";
return;
}
for (const Task& task : tasks) {
std::cout << (task.completed ? "[x] " : "[ ] ")
<< task.description << '\n';
}
}
void addTask(TaskList& tasks)
{
std::cout << "Task description: ";
std::string description;
std::getline(std::cin, description);
tasks.push_back({description, false});
std::cout << "Task added.\n";
}
int main()
{
TaskList tasks;
bool running = true;
while (running) {
std::cout << "\nC++ Task Tracker\n"
<< "1. Add task\n"
<< "2. List tasks\n"
<< "3. Complete task\n"
<< "4. Quit\n";
std::string choice;
std::getline(std::cin, choice);
if (choice == "1") {
addTask(tasks);
} else if (choice == "2") {
listTasks(tasks);
} else if (choice == "4") {
running = false;
std::cout << "Goodbye.\n";
}
}
return 0;
}
Your tracker now holds useful data throughout an interactive session. Next, you will complete numbered tasks while keeping bad input from breaking the menu.
Complete Tasks Safely
Your tracker already keeps tasks in memory through a running menu. Now each displayed number needs to map safely to one task.
Raw numeric extraction leaves std::cin in a failed state when someone enters letters. Further reads keep failing until the program resets the input stream.
You will add reusable input recovery. You will also validate task numbers before accessing the std::vector<Task>.
In this step, get ready to:
- Reveal how the current menu handles unexpected text.
- Build reusable numeric input recovery.
- Complete a numbered task without unsafe vector access.
Expose the current input gap
The current menu accepts text through std::getline. An unrecognized value receives no explanation.
Before you try it, do you think the current menu will explain why hello is invalid?
- Launch the current executable by running this command:
./task-tracker
What does this command do?
This starts the executable built in the previous step. You are testing its current behavior before changing the input logic.
- Enter hello at the menu prompt.
- Enter 4 to quit the tracker.
The menu simply prints again after hello. The missing feedback makes an input mistake look like the app ignored you.
Why numeric input needs recovery
Formatted numeric input can enter a failed state when it receives letters. std::cin.clear() resets that state.
The remaining invalid characters must also leave the stream. clearInputLine() discards them before the next prompt.
Build a reusable input guard
The recovery helper needs the largest supported stream size. The <limits> header provides that value.
- In main.cpp, select the existing three #include lines.
- Replace the selected lines with this import group:
#include <iostream>
#include <limits>
#include <string>
#include <vector>
What does this import add?
The new <limits> header gives the program access to std::numeric_limits<std::streamsize>::max(). That value lets the recovery helper discard the entire remaining input line.
- Find the using TaskList = std::vector<Task>; line.
- Add the input helpers below that line by copying this code:
void clearInputLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
int readNumber(const std::string& prompt)
{
while (true) {
std::cout << prompt;
int value;
if (std::cin >> value) {
clearInputLine();
return value;
}
std::cout << "Please enter a number.\n";
std::cin.clear();
clearInputLine();
}
}
How does the input guard work?
- readNumber() keeps prompting until numeric extraction succeeds.
- std::cin.clear() resets the failed stream state after letters are entered.
- clearInputLine() removes the invalid text before the loop tries again.
- return value; passes the valid number back to the caller.
- Save main.cpp.
- Rebuild the executable with Apple Clang by running this command:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker
What does this build command check?
The command compiles main.cpp in C++17 mode. The warning flags help expose suspicious code before the executable is rebuilt.
The terminal returns to its prompt without compiler warnings. Good progress. Your input helpers now compile cleanly.
Is the build failing?
Check that <limits> appears between <iostream> and <string>. Confirm that both helper functions sit below the TaskList alias.
Ask for help with the exact compiler output if the build still fails: Help me compare my input helper code with these C++ functions and diagnose my compiler error.
Completing a task requires a displayed number. The list must therefore show stable positions that match the vector indexes.
- Find the existing listTasks() function in main.cpp.
- Replace the entire function with this numbered version:
void listTasks(const TaskList& tasks)
{
std::cout << "\nTasks\n";
if (tasks.size() == 0) {
std::cout << "No tasks yet.\n";
return;
}
for (TaskList::size_type index = 0; index < tasks.size(); ++index) {
const Task& task = tasks[index];
std::cout << index + 1 << ". "
<< (task.completed ? "[x] " : "[ ] ")
<< task.description << '\n';
}
}
How do task numbers work?
- index starts at zero because vector positions are zero-based.
- index + 1 shows people a friendlier task number starting at one.
- const Task& task refers to the current task without copying it.
- Save main.cpp.
- Rebuild and launch the numbered tracker by running this command:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker && ./task-tracker
What does this command prove?
The first command rebuilds the executable. The second command starts it only when compilation succeeds.
- Enter 1 to add a task.
- Enter Learn C++ loops as the description.
- Enter 2 to list the tasks.
- Enter 4 to quit.
You will see 1. [ ] Learn C++ loops. The displayed number now identifies the matching vector position.
Still missing the task number?
Confirm that the loop uses TaskList::size_type index. Check that the output begins with index + 1.
Use this prompt if the status marker appears without a number: Help me diagnose why my listTasks function does not show numbered C++ tasks.
An empty description creates a task that cannot be recognized later. The add function needs to reject that value before changing the vector.
- Find the existing addTask() function.
- Replace the entire function with this validated version:
bool addTask(TaskList& tasks)
{
std::cout << "Task description: ";
std::string description;
std::getline(std::cin, description);
if (description.size() == 0) {
std::cout << "A task description cannot be empty.\n";
return false;
}
tasks.push_back({description, false});
std::cout << "Task added.\n";
return true;
}
What does the return value mean?
The function returns false when the description is empty. It returns true only after adding a task.
That success signal becomes useful when later code needs to react only to real changes.
- Save main.cpp.
- Rebuild and launch the validated tracker by running this command:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker && ./task-tracker
What does this check cover?
This rebuild makes the new validation part of the executable. The running menu lets you test the empty-description path immediately.
- Enter 1 at the menu.
- Press Enter without typing a description.
- Enter 4 to quit.
You will see A task description cannot be empty. The vector stays unchanged.
Complete a numbered task
A displayed task number starts at one. Its vector index starts at zero.
The completion function checks both boundaries before subtracting one. This prevents invalid access to the vector.
- Find the closing brace of addTask().
- Add the completion function below addTask() by copying this code:
bool completeTask(TaskList& tasks)
{
if (tasks.size() == 0) {
std::cout << "There are no tasks to complete.\n";
return false;
}
listTasks(tasks);
int taskNumber = readNumber("Task number to complete: ");
if (taskNumber < 1 ||
static_cast<TaskList::size_type>(taskNumber) > tasks.size()) {
std::cout << "That task number does not exist.\n";
return false;
}
TaskList::size_type index =
static_cast<TaskList::size_type>(taskNumber - 1);
tasks[index].completed = true;
std::cout << "Task completed.\n";
return true;
}
How does completion stay safe?
- tasks.size() == 0 stops the function when no task can be selected.
- readNumber() keeps letters from leaving the input stream in a failed state.
- taskNumber < 1 rejects numbers below the displayed range.
- tasks[index].completed = true changes only the selected task after validation succeeds.
The menu now needs to use readNumber() for every choice. A switch routes each valid number to its matching action.
- In main(), find the current std::string choice; line.
- Select the choice input and the complete if chain below it.
- Replace the selected code with this numeric menu logic:
int choice = readNumber("Choose an option: ");
switch (choice) {
case 1:
addTask(tasks);
break;
case 2:
listTasks(tasks);
break;
case 3:
completeTask(tasks);
break;
case 4:
running = false;
std::cout << "Goodbye.\n";
break;
default:
std::cout << "Choose a number from 1 to 4.\n";
}
How does the menu route choices?
readNumber() guarantees that choice contains a number. Each case runs one menu action.
The default branch handles numeric choices outside the menu range. The loop then presents the menu again.
- Save main.cpp.
Before you run the final check, what do you expect the tracker to do after letters and an out-of-range task number?
- Rebuild and launch the completed tracker by running this command:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker && ./task-tracker
What does the final command verify?
The build confirms that every new function connects correctly. The launch starts the exact executable you just rebuilt.
- Enter hello at the menu prompt.
- Enter 1 at the repeated prompt.
- Enter Learn C++ loops as the task description.
- Enter 3 to choose task completion.
- Enter 99 as the task number.
- Enter 3 to try task completion again.
- Enter 1 as the task number.
- Enter 2 to list the completed task.
The tracker prints Please enter a number. after hello. It prints That task number does not exist. after 99.
Your final list shows 1. [x] Learn C++ loops. That is the risky input path handled. The tracker now recovers from mistakes without losing control of its menu.
- Enter 4 to quit the tracker.
Is the menu repeating forever?
Confirm that std::cin.clear() runs before the second clearInputLine() call. Check that main() calls readNumber() instead of reading a string.
Use this prompt if invalid input still traps the menu: Help me trace why my C++ input stream does not recover after I enter letters.
✔️ Awesome, I've got everything!
Your tracker now rejects invalid input. It can also mark a selected in-memory task complete.
ⓧ I'd like to double check the full code
Compare your saved main.cpp with the complete file below.
#include <iostream>
#include <limits>
#include <string>
#include <vector>
struct Task {
std::string description;
bool completed = false;
};
using TaskList = std::vector<Task>;
void clearInputLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
int readNumber(const std::string& prompt)
{
while (true) {
std::cout << prompt;
int value;
if (std::cin >> value) {
clearInputLine();
return value;
}
std::cout << "Please enter a number.\n";
std::cin.clear();
clearInputLine();
}
}
void listTasks(const TaskList& tasks)
{
std::cout << "\nTasks\n";
if (tasks.size() == 0) {
std::cout << "No tasks yet.\n";
return;
}
for (TaskList::size_type index = 0; index < tasks.size(); ++index) {
const Task& task = tasks[index];
std::cout << index + 1 << ". "
<< (task.completed ? "[x] " : "[ ] ")
<< task.description << '\n';
}
}
bool addTask(TaskList& tasks)
{
std::cout << "Task description: ";
std::string description;
std::getline(std::cin, description);
if (description.size() == 0) {
std::cout << "A task description cannot be empty.\n";
return false;
}
tasks.push_back({description, false});
std::cout << "Task added.\n";
return true;
}
bool completeTask(TaskList& tasks)
{
if (tasks.size() == 0) {
std::cout << "There are no tasks to complete.\n";
return false;
}
listTasks(tasks);
int taskNumber = readNumber("Task number to complete: ");
if (taskNumber < 1 ||
static_cast<TaskList::size_type>(taskNumber) > tasks.size()) {
std::cout << "That task number does not exist.\n";
return false;
}
TaskList::size_type index =
static_cast<TaskList::size_type>(taskNumber - 1);
tasks[index].completed = true;
std::cout << "Task completed.\n";
return true;
}
int main()
{
TaskList tasks;
bool running = true;
while (running) {
std::cout << "\nC++ Task Tracker\n"
<< "1. Add task\n"
<< "2. List tasks\n"
<< "3. Complete task\n"
<< "4. Quit\n";
int choice = readNumber("Choose an option: ");
switch (choice) {
case 1:
addTask(tasks);
break;
case 2:
listTasks(tasks);
break;
case 3:
completeTask(tasks);
break;
case 4:
running = false;
std::cout << "Goodbye.\n";
break;
default:
std::cout << "Choose a number from 1 to 4.\n";
}
}
return 0;
}
Use this full file as a comparison reference. Save any corrections before rebuilding the executable again.
Your tracker can now trust its menu numbers without trusting every keystroke. Next, you will save each successful change so completed tasks survive a restart.
Persist Tasks to a File
Your tracker now recovers from bad input. It can also mark a chosen task complete in memory.
Those tasks still disappear when the process ends because process memory is temporary. Standard file streams give the same task state a home between runs.
In this step, get ready to:
- Save task descriptions plus completion flags in a local file.
- Load saved task records when the tracker starts.
- Prove a completed task survives a restart.
Save task changes to disk
An output file stream writes program data into a local file. Each task uses two lines in tasks.txt. The first line stores its completion flag. The second line stores its full description.
- In main.cpp, add the file stream header above #include <iostream> by inserting this line:
#include <fstream>
Why Add This Header?
The <fstream> header provides the standard file stream types used to read from files or write to them.
- In main.cpp, find the closing brace of completeTask.
- Add saveTasks below completeTask by copying this code:
void saveTasks(const TaskList& tasks)
{
std::ofstream output("tasks.txt");
if (!output) {
std::cerr << "Could not save tasks.\n";
return;
}
for (const Task& task : tasks) {
output << task.completed << '\n';
output << task.description << '\n';
}
}
What Does This Function Do?
- The std::ofstream object opens tasks.txt for writing.
- The stream check stops the function when the file cannot be opened.
- The loop writes each completed flag on its own line.
- The next output statement preserves the full task description on a separate line.
- Save main.cpp.
- Confirm the new file stream code compiles by running this command:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker
What Does This Check Prove?
This rebuild confirms that <fstream> plus saveTasks compile successfully with the rest of your tracker.
A successful build returns you to the terminal prompt with a rebuilt task-tracker executable.
Having Trouble Rebuilding?
- Check that #include <fstream> appears before the other headers.
- Check that every opening brace in saveTasks has a matching closing brace.
- Check that both output lines end with a semicolon.
Ask for help with the compiler output: Why is my saveTasks function failing to compile?
- In main.cpp, find the existing case 1: block.
- Replace that block with this version:
case 1:
if (addTask(tasks)) {
saveTasks(tasks);
}
break;
Why Check the Result?
The addTask function returns true only after it adds a valid description. This condition saves the list after a successful change.
- Save main.cpp.
Before you run the tracker, predict whether tasks.txt appears before or after a successful Add action.
- Rebuild the tracker plus launch it by running this command:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker && ./task-tracker
What Does This Command Do?
The first command rebuilds the executable from main.cpp. The second command launches the rebuilt tracker after compilation succeeds.
- Enter 1 at the menu.
- Enter Buy groceries as the task description.
- Enter 4 to quit.
- Select tasks.txt from the file list in VS Code.
You will see 0 on the first line. You will see Buy groceries on the second line.
Cannot Find tasks.txt?
- Confirm that the tracker printed Task added. after you entered the description.
- Confirm that saveTasks(tasks); sits inside the successful addTask condition.
- Confirm that the integrated terminal remains in the cpp-task-tracker folder.
Get help tracing the save path: Why did my C++ tracker fail to create tasks.txt after adding a task?
That is your first durable task. The description now has a life beyond one run of the executable.
Load tasks at startup
An input file stream rebuilds the in-memory task list from saved records. A missing first-run file represents an empty task list.
- In main.cpp, find the closing brace of saveTasks.
- Add loadTasks below saveTasks by copying this code:
void loadTasks(TaskList& tasks)
{
std::ifstream input("tasks.txt");
if (!input) {
return;
}
bool completed;
std::string description;
while (input >> completed) {
input.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
if (!std::getline(input, description)) {
break;
}
tasks.push_back({description, completed});
}
}
How Does Loading Work?
- The std::ifstream object opens tasks.txt for reading.
- The early return lets the tracker start with an empty list when the file does not exist.
- The extraction reads one completion flag from each record.
- The ignore call removes the remaining line break before std::getline reads the full description.
- The push_back call restores the saved values as a Task.
- In main, find TaskList tasks;.
- Add this call directly below that line:
loadTasks(tasks);
Why Load Here?
This call restores saved tasks immediately after the vector is created. The menu therefore starts with the recovered data already available.
- Save main.cpp.
Before you rebuild, predict what the List option displays when the program reads your existing tasks.txt file.
- Rebuild the tracker plus launch it by running this command:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker && ./task-tracker
What Is Being Tested?
The rebuilt tracker calls loadTasks before displaying the menu. This run tests whether the saved record becomes an in-memory Task again.
- Enter 2 to list the loaded tasks.
- Enter 4 to quit.
You will see 1. [ ] Buy groceries. The description has survived the first restart.
Seeing an Empty Task List?
- Check that loadTasks(tasks); appears immediately after TaskList tasks;.
- Check that tasks.txt remains inside the cpp-task-tracker folder.
- Check that the file contains a completion flag above the description.
Ask for help tracing the load process: Why does loadTasks leave my C++ task list empty?
Prove completion survives a restart
Adding a task now saves the vector. Completing a task also changes persistent state, so that successful branch needs the same save behavior.
- In main.cpp, find the existing case 3: block.
- Replace that block with this version:
case 3:
if (completeTask(tasks)) {
saveTasks(tasks);
}
break;
Why Save Only Successful Completions?
The completeTask function returns true only after it updates a valid task. Invalid numbers leave the file unchanged.
- Save main.cpp.
Before you test the change, predict which completion flag tasks.txt stores after task 1 is completed.
- Rebuild the tracker plus launch it by running this command:
clang++ -std=c++17 -Wall -Wextra main.cpp -o task-tracker && ./task-tracker
What Does This Run Check?
This run uses the rebuilt case 3 branch. A successful completion now calls saveTasks before control returns to the menu.
- Enter 3 at the menu.
- Enter 1 as the task number.
- Enter 4 to quit.
You will see Task completed. before the menu returns.
Before you restart the executable, predict whether the tracker restores task 1 as pending or complete.
- Restart the existing executable by running this command:
./task-tracker
Why Is This the Persistence Test?
Restarting creates a new process with fresh memory. The restored status can only come from the completion flag saved in tasks.txt.
- Enter 2 to list the restored tasks.
- Enter 4 to quit.
- Select tasks.txt from the file list in VS Code.
You will see 1. [x] Buy groceries in the terminal. The file shows 1 above Buy groceries.
Task Returns as Pending?
- Check that saveTasks(tasks); sits inside the successful completeTask condition.
- Check that you rebuilt task-tracker after editing case 3.
- Check that the first line for the completed task is 1 in tasks.txt.
Get help following the status through the file: Why does my completed C++ task return as pending after restart?
You have closed the persistence loop. The tracker now restores both the task description and its completed state after a full restart.
✔️ Awesome, I've got everything!
Great work. Double check that you saved main.cpp after proving the [x] status survives a restart.
ⓧ I'd like to double check the full code
Compare your main.cpp file with this complete version:
#include <fstream>
#include <iostream>
#include <limits>
#include <string>
#include <vector>
struct Task {
std::string description;
bool completed = false;
};
using TaskList = std::vector<Task>;
void clearInputLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
int readNumber(const std::string& prompt)
{
while (true) {
std::cout << prompt;
int value;
if (std::cin >> value) {
clearInputLine();
return value;
}
std::cout << "Please enter a number.\n";
std::cin.clear();
clearInputLine();
}
}
void listTasks(const TaskList& tasks)
{
std::cout << "\nTasks\n";
if (tasks.size() == 0) {
std::cout << "No tasks yet.\n";
return;
}
for (TaskList::size_type index = 0; index < tasks.size(); ++index) {
const Task& task = tasks[index];
std::cout << index + 1 << ". "
<< (task.completed ? "[x] " : "[ ] ")
<< task.description << '\n';
}
}
bool addTask(TaskList& tasks)
{
std::cout << "Task description: ";
std::string description;
std::getline(std::cin, description);
if (description.size() == 0) {
std::cout << "A task description cannot be empty.\n";
return false;
}
tasks.push_back({description, false});
std::cout << "Task added.\n";
return true;
}
bool completeTask(TaskList& tasks)
{
if (tasks.size() == 0) {
std::cout << "There are no tasks to complete.\n";
return false;
}
listTasks(tasks);
int taskNumber = readNumber("Task number to complete: ");
if (taskNumber < 1 ||
static_cast<TaskList::size_type>(taskNumber) > tasks.size()) {
std::cout << "That task number does not exist.\n";
return false;
}
TaskList::size_type index =
static_cast<TaskList::size_type>(taskNumber - 1);
tasks[index].completed = true;
std::cout << "Task completed.\n";
return true;
}
void saveTasks(const TaskList& tasks)
{
std::ofstream output("tasks.txt");
if (!output) {
std::cerr << "Could not save tasks.\n";
return;
}
for (const Task& task : tasks) {
output << task.completed << '\n';
output << task.description << '\n';
}
}
void loadTasks(TaskList& tasks)
{
std::ifstream input("tasks.txt");
if (!input) {
return;
}
bool completed;
std::string description;
while (input >> completed) {
input.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
if (!std::getline(input, description)) {
break;
}
tasks.push_back({description, completed});
}
}
int main()
{
TaskList tasks;
loadTasks(tasks);
bool running = true;
while (running) {
std::cout << "\nC++ Task Tracker\n"
<< "1. Add task\n"
<< "2. List tasks\n"
<< "3. Complete task\n"
<< "4. Quit\n";
int choice = readNumber("Choose an option: ");
switch (choice) {
case 1:
if (addTask(tasks)) {
saveTasks(tasks);
}
break;
case 2:
listTasks(tasks);
break;
case 3:
if (completeTask(tasks)) {
saveTasks(tasks);
}
break;
case 4:
running = false;
std::cout << "Goodbye.\n";
break;
default:
std::cout << "Choose a number from 1 to 4.\n";
}
}
return 0;
}
What Should Match?
This complete file includes both persistence functions. It loads once before the menu loop. It saves after each successful Add or Complete action.
Secret mission
Show a Completion Progress Summary
Your tracker already stores the information needed to measure progress. Extend an existing TaskList by adding a read-only menu option that reports completed tasks out of the total.
Clean Up Your Resources
Clean Up Your Resources
Everything you built stays on your Mac, so there are no ongoing costs. Choose whether to keep the project, pause it for later, or delete it entirely.
Resources you used:
- The main.cpp source file inside cpp-task-tracker.
- The compiled task-tracker executable inside cpp-task-tracker.
- The tasks.txt data file inside cpp-task-tracker.
Keep everything running
No action needed. Choose this if you want to keep using or extending your task tracker.
- Keep the cpp-task-tracker folder intact.
- Keep main.cpp for future C++ changes.
- Keep task-tracker ready for another terminal session.
- Keep tasks.txt so your saved tasks remain available.
Pause - I'll come back to this later
Nothing keeps running after you quit the tracker. Your local files remain ready for your next session.
- Enter 5 at the tracker menu if the app is still running.
- Leave the cpp-task-tracker folder unchanged.
Delete - I don't want to use this again
Deleting the folder removes main.cpp, task-tracker, and tasks.txt together. This only affects your local project.
- Enter 5 at the tracker menu if the app is still running.
- Switch to Finder.
- Locate the cpp-task-tracker folder you created earlier.
- Drag the cpp-task-tracker folder to the Trash.
- Empty the Trash to remove the folder permanently.
- Confirm that cpp-task-tracker no longer appears in Finder.
Nice Work!
Nice Work!
You did it! You built a persistent C++ task tracker that restores saved tasks after every restart.
You've learned how to:
- Compile main.cpp into the local task-tracker executable with Apple Clang in C++17 mode.
- Keep the interactive menu responsive with safe input recovery plus range checks before task access.
- Use local file persistence to restore task descriptions plus completion states from tasks.txt after restarting the executable.
- Secret Mission: Add a read-only completion progress summary based on the existing task states.
Ready to quiz yourself?