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.
- 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 }}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
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.