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
Itemclass representing one product: ID, name, quantity, and price. - A
template <typename T> T getValidatedInput(...)function reused for both quantities and prices. - An
Inventoryclass managing astd::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::ifstreamso inventory survives between runs.
Prerequisites
- Classes —
privatefields, constructors, and member functions. - STL containers —
std::map<Key, Value>, itsfind()/begin()/end()interface, and iterating with a range-basedforloop. - Templates — the basic shape of
template <typename T> ReturnType functionName(...). - File handling in C++ —
std::ofstream/std::ifstreamand parsing text withstd::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
Supplierfield toItemand a report grouping total inventory value by supplier using astd::map<std::string, double>. - Log every restock and order to a separate
transactions.txtfile, 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.