LearnAI ToolsCareerPractice BuildsPlayContact
C++ ProgrammingIntermediate~2.5 hours

Inventory Management System

Manage stock and orders efficiently using STL containers like vectors and maps.

STLTemplatesFile Handling

Overview

An inventory system spends most of its time doing one thing: looking up a product by ID and changing its quantity, whether that is restocking shelves or shipping out an order. That access pattern — look up by a unique key, then read or update — is exactly what `std::map` is built for, and it is a natural upgrade from the array-based lookups you may have used in earlier projects, since `std::map` keeps every product sorted by ID automatically and finds any entry in logarithmic time no matter how large the catalog grows.

By the end of this tutorial you will have a console inventory application built around an `Item` class, a `std::map<int, Item>` keyed by product ID, and a small but genuinely reusable template function that validates numeric input — the same validation loop working for both integer quantities and floating-point prices without being written twice. Inventory data is also persisted to disk with `<fstream>`, so stock levels survive between runs.

What You'll Build
  • An `Item` class representing one product: ID, name, quantity, and price.
  • A `template <typename T> T getValidatedInput(...)` function reused for both quantities and prices.
  • An `Inventory` class managing a `std::map<int, Item>` keyed by product ID.
  • Restock and "place order" operations that add or safely subtract stock.
  • A low-stock report that scans the map for items below a chosen threshold.
  • File persistence with `std::ofstream`/`std::ifstream` so inventory survives between runs.

Prerequisites

  • Classes — `private` fields, constructors, and member functions.
  • STL containers — `std::map<Key, Value>`, its `find()`/`begin()`/`end()` interface, and iterating with a range-based `for` loop.
  • Templates — the basic shape of `template <typename T> ReturnType functionName(...)`.
  • File handling in C++ — `std::ofstream`/`std::ifstream` and parsing text with `std::stringstream`.
  • Basic validation logic — checking `std::cin.fail()` after a read and clearing the stream to recover.

Project Structure

The program lives in a single file, `inventory.cpp`. `Item` is a small value class holding one product's data; it never talks to the console or the file system itself, keeping it easy to reuse or test in isolation. `Inventory` owns a `std::map<int, Item>`, where the key is the product ID and the value is the `Item` itself — choosing a map over a `std::vector<Item>` here has a real, practical benefit: because `std::map` is node-based internally, adding a brand-new item never invalidates a pointer you already obtained from `find()` on a different item, unlike a `std::vector`, which can reallocate its entire buffer and invalidate every pointer into it when it grows.

The one genuinely generic piece of this project is `getValidatedInput<T>`, a function template that reads a value of type `T` and keeps re-prompting until it is both numeric and at least a caller-supplied minimum. It is instantiated twice in this project — once as `getValidatedInput<int>` for quantities, once as `getValidatedInput<double>` for prices — and the compiler generates a separate, type-specific version of the function for each, without you writing either loop by hand twice.

Step 1: Define the Item Class

Every field an item needs — ID, name, quantity, price — lives behind `private` access, with `addStock`/`removeStock` as the only ways quantity ever changes. `removeStock` returns `bool` rather than blindly subtracting, so callers can reject an order that would take stock negative.

#include <iostream> // std::cin/std::cout for all console interaction
#include <fstream> // std::ifstream/std::ofstream for reading and writing inventory data to disk
#include <sstream> // std::stringstream for splitting a saved comma-separated line back into fields
#include <iomanip> // std::fixed/std::setprecision for two-decimal price formatting
#include <string> // std::string for item names and std::stoi/std::stod for parsing
#include <map> // std::map<int, Item> keyed by product ID
class Item {
private:
int id; // Unique product ID, also the key used to store this item in the Inventory's map
std::string name; // Product name
int quantity; // Units currently in stock
double price; // Price per unit
public:
Item() : id(0), quantity(0), price(0.0) {} // Default constructor: std::map needs this to create empty slots internally
Item(int id, const std::string &name, int quantity, double price)
: id(id), name(name), quantity(quantity), price(price) {}
int getId() const { return id; }
std::string getName() const { return name; }
int getQuantity() const { return quantity; }
double getPrice() const { return price; }
void addStock(int amount) { quantity += amount; } // Restocking always increases quantity
bool removeStock(int amount) { // Returns false instead of ever allowing negative stock
if (amount > quantity) {
return false;
}
quantity -= amount;
return true;
}
double totalValue() const { return quantity * price; } // Convenience calculation used in reports
void display() const {
std::cout << id << "\t" << name << "\t" << quantity << "\t"
<< price << "\t" << totalValue() << "\n";
}
};

Step 2: Write a Generic Input-Validation Template

Without a template, validating a quantity (`int`) and validating a price (`double`) would mean writing the exact same "read, check for failure, check the minimum, retry" loop twice with only the type changed. `template <typename T>` lets the compiler generate both versions from one function body, so the logic is written, tested, and fixed in exactly one place.

// Generic input-validation helper: reads a value of type T and keeps re-prompting
// until it is numeric and at least minValue. One template serves both int quantities
// and double prices, instead of duplicating this loop for each type.
template <typename T>
T getValidatedInput(const std::string &prompt, T minValue) {
T value;
while (true) {
std::cout << prompt;
std::cin >> value;
if (std::cin.fail()) { // Non-numeric input, e.g. the user typed letters
std::cin.clear(); // Clear the stream's fail flag so later reads still work
std::cin.ignore(10000, '\n'); // Discard the bad input still sitting in the buffer
std::cout << "Invalid input, please enter a number.\n";
continue;
}
if (value < minValue) { // Enforce the caller's minimum (e.g. quantity >= 0)
std::cout << "Value must be at least " << minValue << ".\n";
continue;
}
return value; // Only reached once a valid, in-range value was read
}
}
Two Instantiations of the Same Template

Click Run to see what this code prints.

Step 3: Build the Inventory Class Around a Map

`addItem` uses `items[nextId] = ...`, which inserts a brand-new entry because `nextId` has never been used as a key before. `findItem` deliberately uses `items.find(id)` rather than `items[id]` — `operator[]` would silently insert a default-constructed `Item` for a missing ID, which is exactly the bug `find()` avoids, since `find()` only ever looks, never inserts.

class Inventory {
private:
std::map<int, Item> items; // Keyed by product ID; std::map keeps entries sorted by key automatically
int nextId; // Auto-incrementing counter used to assign new product IDs
public:
Inventory() : nextId(1) {} // First item added this run will be product ID 1
void addItem(const std::string &name, int quantity, double price); // Defined in this step
Item* findItem(int id); // Defined in this step
void listItems() const; // Defined in this step
void restock(int id, int amount); // Defined in Step 4
void placeOrder(int id, int amount); // Defined in Step 4
void lowStockReport(int threshold) const; // Defined in Step 4
void saveToFile(const std::string &filename) const; // Defined in Step 5
void loadFromFile(const std::string &filename); // Defined in Step 5
};
void Inventory::addItem(const std::string &name, int quantity, double price) {
items[nextId] = Item(nextId, name, quantity, price); // operator[] inserts a new entry at key nextId
std::cout << "Added \"" << name << "\" with product ID " << nextId << ".\n";
nextId++; // Guarantees the next item gets a fresh, unused ID
}
Item* Inventory::findItem(int id) {
std::map<int, Item>::iterator it = items.find(id); // find() looks up without inserting a default entry
if (it == items.end()) { // end() means no entry with that key exists
return nullptr;
}
return &it->second; // ->second is the Item stored at this key; ->first would be the key itself
}
void Inventory::listItems() const {
std::cout << "\nID\tName\tQty\tPrice\tTotal Value\n";
for (std::map<int, Item>::const_iterator it = items.begin(); it != items.end(); ++it) { // Ascending key order, guaranteed by std::map
it->second.display();
}
}

Step 4: Restock, Place Orders, and Report Low Stock

`restock` and `placeOrder` both start by finding the item and bailing out if it does not exist, then delegate the actual quantity change to `Item::addStock`/`Item::removeStock` — `Inventory` never touches `quantity` directly, it just orchestrates the lookup and reports the result. `lowStockReport` reuses the same map-iteration pattern from `listItems`, adding one condition to filter which items get printed.

void Inventory::restock(int id, int amount) {
Item *item = findItem(id);
if (item == nullptr) {
std::cout << "Item not found.\n";
return;
}
item->addStock(amount);
std::cout << "Restocked " << amount << " units of \"" << item->getName() << "\".\n";
}
void Inventory::placeOrder(int id, int amount) { // "Placing an order" removes stock as it ships out
Item *item = findItem(id);
if (item == nullptr) {
std::cout << "Item not found.\n";
return;
}
if (!item->removeStock(amount)) {
std::cout << "Not enough stock. Available: " << item->getQuantity() << "\n";
return;
}
std::cout << "Order placed: " << amount << " units of \"" << item->getName() << "\".\n";
}
void Inventory::lowStockReport(int threshold) const {
std::cout << "\n--- Low Stock Items (below " << threshold << ") ---\n";
for (std::map<int, Item>::const_iterator it = items.begin(); it != items.end(); ++it) {
if (it->second.getQuantity() < threshold) { // Only print items under the threshold
it->second.display();
}
}
}

Step 5: Persist the Inventory to a File

Saving and loading follow the same comma-separated text pattern used elsewhere in this course: one line per item, split back into fields with `std::stringstream` on load. As with the Bank Management System project, loading also tracks the highest ID seen so `nextId` resumes correctly instead of colliding with an item already on disk.

void Inventory::saveToFile(const std::string &filename) const {
std::ofstream out(filename); // ofstream creates the file if missing, truncates it if it already exists
for (std::map<int, Item>::const_iterator it = items.begin(); it != items.end(); ++it) {
const Item &item = it->second;
out << item.getId() << "," << item.getName() << "," << item.getQuantity() << "," << item.getPrice() << "\n";
}
out.close();
}
void Inventory::loadFromFile(const std::string &filename) {
std::ifstream in(filename);
if (!in.is_open()) { // No file yet means this is the first run; nothing to load
return;
}
std::string line;
int highestSeen = 0;
while (std::getline(in, line)) { // Read one comma-separated record per line
std::stringstream ss(line);
std::string idStr, name, qtyStr, priceStr;
std::getline(ss, idStr, ',');
std::getline(ss, name, ',');
std::getline(ss, qtyStr, ',');
std::getline(ss, priceStr, ',');
int id = std::stoi(idStr); // Convert the text fields back into their real types
int qty = std::stoi(qtyStr);
double price = std::stod(priceStr);
items[id] = Item(id, name, qty, price);
if (id > highestSeen) {
highestSeen = id;
}
}
nextId = highestSeen + 1; // Resume numbering after the highest ID loaded from disk
in.close();
}

Step 6: Build the Menu Loop

`main` loads any existing inventory, then loops over a numbered menu, using `getValidatedInput<int>`/`getValidatedInput<double>` everywhere a quantity or price needs to be read so bad input never crashes the program or corrupts the map.

int main() {
Inventory inventory;
const std::string filename = "inventory.txt";
inventory.loadFromFile(filename); // Restore whatever stock levels were saved from a previous run
std::cout << std::fixed << std::setprecision(2); // All prices/values print with exactly two decimals from here on
int choice;
do {
std::cout << "\n===== INVENTORY MANAGEMENT SYSTEM =====\n";
std::cout << "1. Add Item\n";
std::cout << "2. Restock Item\n";
std::cout << "3. Place Order (Remove Stock)\n";
std::cout << "4. List Items\n";
std::cout << "5. Low Stock Report\n";
std::cout << "6. Exit\n";
std::cout << "Enter your choice: ";
std::cin >> choice;
if (choice == 1) {
std::string name;
std::cout << "Enter item name: ";
std::cin.ignore(); // Discard the leftover newline left behind by "std::cin >> choice"
std::getline(std::cin, name);
int quantity = getValidatedInput<int>("Enter starting quantity: ", 0); // Template instantiated for int
double price = getValidatedInput<double>("Enter price per unit: ", 0.0); // Same template, instantiated for double
inventory.addItem(name, quantity, price);
inventory.saveToFile(filename);
} else if (choice == 2) {
int id = getValidatedInput<int>("Enter item ID to restock: ", 1);
int amount = getValidatedInput<int>("Enter quantity to add: ", 1);
inventory.restock(id, amount);
inventory.saveToFile(filename);
} else if (choice == 3) {
int id = getValidatedInput<int>("Enter item ID to order: ", 1);
int amount = getValidatedInput<int>("Enter quantity to remove: ", 1);
inventory.placeOrder(id, amount);
inventory.saveToFile(filename);
} else if (choice == 4) {
inventory.listItems();
} else if (choice == 5) {
int threshold = getValidatedInput<int>("Enter low-stock threshold: ", 1);
inventory.lowStockReport(threshold);
} else if (choice == 6) {
std::cout << "Goodbye!\n";
} else {
std::cout << "Invalid choice, try again.\n";
}
} while (choice != 6); // Keep looping until the user explicitly picks Exit
return 0;
}

Complete Code

Here is the full program, ready to compile with `g++ inventory.cpp -o inventory`.

#include <iostream> // std::cin/std::cout for all console interaction
#include <fstream> // std::ifstream/std::ofstream for reading and writing inventory data to disk
#include <sstream> // std::stringstream for splitting a saved comma-separated line back into fields
#include <iomanip> // std::fixed/std::setprecision for two-decimal price formatting
#include <string> // std::string for item names and std::stoi/std::stod for parsing
#include <map> // std::map<int, Item> keyed by product ID
class Item {
private:
int id;
std::string name;
int quantity;
double price;
public:
Item() : id(0), quantity(0), price(0.0) {}
Item(int id, const std::string &name, int quantity, double price)
: id(id), name(name), quantity(quantity), price(price) {}
int getId() const { return id; }
std::string getName() const { return name; }
int getQuantity() const { return quantity; }
double getPrice() const { return price; }
void addStock(int amount) { quantity += amount; }
bool removeStock(int amount) {
if (amount > quantity) {
return false;
}
quantity -= amount;
return true;
}
double totalValue() const { return quantity * price; }
void display() const {
std::cout << id << "\t" << name << "\t" << quantity << "\t"
<< price << "\t" << totalValue() << "\n";
}
};
template <typename T>
T getValidatedInput(const std::string &prompt, T minValue) {
T value;
while (true) {
std::cout << prompt;
std::cin >> value;
if (std::cin.fail()) {
std::cin.clear();
std::cin.ignore(10000, '\n');
std::cout << "Invalid input, please enter a number.\n";
continue;
}
if (value < minValue) {
std::cout << "Value must be at least " << minValue << ".\n";
continue;
}
return value;
}
}
class Inventory {
private:
std::map<int, Item> items;
int nextId;
public:
Inventory() : nextId(1) {}
void addItem(const std::string &name, int quantity, double price);
Item* findItem(int id);
void listItems() const;
void restock(int id, int amount);
void placeOrder(int id, int amount);
void lowStockReport(int threshold) const;
void saveToFile(const std::string &filename) const;
void loadFromFile(const std::string &filename);
};
void Inventory::addItem(const std::string &name, int quantity, double price) {
items[nextId] = Item(nextId, name, quantity, price);
std::cout << "Added \"" << name << "\" with product ID " << nextId << ".\n";
nextId++;
}
Item* Inventory::findItem(int id) {
std::map<int, Item>::iterator it = items.find(id);
if (it == items.end()) {
return nullptr;
}
return &it->second;
}
void Inventory::listItems() const {
std::cout << "\nID\tName\tQty\tPrice\tTotal Value\n";
for (std::map<int, Item>::const_iterator it = items.begin(); it != items.end(); ++it) {
it->second.display();
}
}
void Inventory::restock(int id, int amount) {
Item *item = findItem(id);
if (item == nullptr) {
std::cout << "Item not found.\n";
return;
}
item->addStock(amount);
std::cout << "Restocked " << amount << " units of \"" << item->getName() << "\".\n";
}
void Inventory::placeOrder(int id, int amount) {
Item *item = findItem(id);
if (item == nullptr) {
std::cout << "Item not found.\n";
return;
}
if (!item->removeStock(amount)) {
std::cout << "Not enough stock. Available: " << item->getQuantity() << "\n";
return;
}
std::cout << "Order placed: " << amount << " units of \"" << item->getName() << "\".\n";
}
void Inventory::lowStockReport(int threshold) const {
std::cout << "\n--- Low Stock Items (below " << threshold << ") ---\n";
for (std::map<int, Item>::const_iterator it = items.begin(); it != items.end(); ++it) {
if (it->second.getQuantity() < threshold) {
it->second.display();
}
}
}
void Inventory::saveToFile(const std::string &filename) const {
std::ofstream out(filename);
for (std::map<int, Item>::const_iterator it = items.begin(); it != items.end(); ++it) {
const Item &item = it->second;
out << item.getId() << "," << item.getName() << "," << item.getQuantity() << "," << item.getPrice() << "\n";
}
out.close();
}
void Inventory::loadFromFile(const std::string &filename) {
std::ifstream in(filename);
if (!in.is_open()) {
return;
}
std::string line;
int highestSeen = 0;
while (std::getline(in, line)) {
std::stringstream ss(line);
std::string idStr, name, qtyStr, priceStr;
std::getline(ss, idStr, ',');
std::getline(ss, name, ',');
std::getline(ss, qtyStr, ',');
std::getline(ss, priceStr, ',');
int id = std::stoi(idStr);
int qty = std::stoi(qtyStr);
double price = std::stod(priceStr);
items[id] = Item(id, name, qty, price);
if (id > highestSeen) {
highestSeen = id;
}
}
nextId = highestSeen + 1;
in.close();
}
int main() {
Inventory inventory;
const std::string filename = "inventory.txt";
inventory.loadFromFile(filename);
std::cout << std::fixed << std::setprecision(2);
int choice;
do {
std::cout << "\n===== INVENTORY MANAGEMENT SYSTEM =====\n";
std::cout << "1. Add Item\n";
std::cout << "2. Restock Item\n";
std::cout << "3. Place Order (Remove Stock)\n";
std::cout << "4. List Items\n";
std::cout << "5. Low Stock Report\n";
std::cout << "6. Exit\n";
std::cout << "Enter your choice: ";
std::cin >> choice;
if (choice == 1) {
std::string name;
std::cout << "Enter item name: ";
std::cin.ignore();
std::getline(std::cin, name);
int quantity = getValidatedInput<int>("Enter starting quantity: ", 0);
double price = getValidatedInput<double>("Enter price per unit: ", 0.0);
inventory.addItem(name, quantity, price);
inventory.saveToFile(filename);
} else if (choice == 2) {
int id = getValidatedInput<int>("Enter item ID to restock: ", 1);
int amount = getValidatedInput<int>("Enter quantity to add: ", 1);
inventory.restock(id, amount);
inventory.saveToFile(filename);
} else if (choice == 3) {
int id = getValidatedInput<int>("Enter item ID to order: ", 1);
int amount = getValidatedInput<int>("Enter quantity to remove: ", 1);
inventory.placeOrder(id, amount);
inventory.saveToFile(filename);
} else if (choice == 4) {
inventory.listItems();
} else if (choice == 5) {
int threshold = getValidatedInput<int>("Enter low-stock threshold: ", 1);
inventory.lowStockReport(threshold);
} else if (choice == 6) {
std::cout << "Goodbye!\n";
} else {
std::cout << "Invalid choice, try again.\n";
}
} while (choice != 6);
return 0;
}

Sample Run

Sample Run

Click Run to see what this code prints.

Extend This Project

  • Add a `Supplier` field to `Item` and a report grouping total inventory value by supplier using a `std::map<std::string, double>`.
  • Log every restock and order to a separate `transactions.txt` file, similar to the transaction history in the Bank Management System project.
  • Add a second template function, `template <typename T> T findMax(const std::vector<T> &values)`, and use it to report the single most valuable item in stock.
  • Support categories (e.g. "Electronics", "Stationery") using `std::map<std::string, std::vector<int>>` mapping each category to the product IDs in it.
  • Add an "Order History" report that keeps a running total of units sold per item, separate from current stock on hand.

Summary

You built an inventory system where `std::map` does the lookup work an array-and-loop approach would otherwise require by hand, and where one small template function replaced two nearly-identical validation loops. Reaching for the STL container that matches your actual access pattern — keyed lookup here, sequence elsewhere — and reaching for a template when the same logic needs to work across more than one type, are two habits that will make every C++ project after this one noticeably shorter and more reliable.