Mastering C++ STL: Online Store Modelling and Employee Management Using Containers and Lambdas

Building an Online Store with C++ STL Containers

Introduction

Today, we'll explore how to implement the foundational elements of an online store using C++ and its powerful Standard Template Library (STL) containers. While real e-commerce platforms typically use web technologies and databases, understanding how to model such systems at a lower level can provide valuable insights into data structure selection and system design.

The Data Model

Our online store has two primary data structures:

struct Product{
    int productID;
    string name;
    string category;
};

struct Order{
    int orderID;
    int productID;
    int quantity;
    string customerID;
    time_t orderDate;
};

These structures represent the core entities in our system: products available for purchase and orders placed by customers.

Leveraging STL Containers

The power of C++ STL lies in its diverse container types, each optimized for specific operations. Let's see how we've utilized them:

1. Vector for Product Catalogue

vector<Product> products = {
    {101,"Laptop","Electronics"},
    {102,"SmartPhone","Electronics"},
    {103,"Coffe Maker","Kitchen"},
    {104,"Blender","Kitchen"},
    {105,"Desk Lamp","Home"}
};

Vectors provide dynamic arrays with random access, perfect for our product catalogue where we need indexed access and sequential traversal.

2. Deque for Recent Customer Tracking

deque<string> recentCustomers = {"C001","C002","C003"};
recentCustomers.push_back("C004");
recentCustomers.push_front("C004");

A double-ended queue (deque) allows efficient insertion and deletion at both ends, making it ideal for tracking recent customers in a FIFO (First-In-First-Out) manner, or implementing features like "Recently Viewed."

3. List for Order History

list<Order> orderHistory;
orderHistory.push_back({1,101,2,"C001",time(0)});
orderHistory.push_back({2,102,2,"C002",time(0)});
orderHistory.push_back({3,103,1,"C003",time(0)});

Lists excel at insertion and removal operations anywhere in the sequence, suitable for order history that might require frequent updates.

4. Set for Unique Categories

set<string> categories;
for(const auto &product:products){
    categories.insert(product.category);
}

Sets automatically maintain unique elements in a sorted order, perfect for extracting unique product categories without duplicates.

5. Map for Inventory Management

map<int,int> productStock = {
    {101,10},
    {102,20},
    {103,15},
    {104,5},
    {105,7}
};

Maps provide key-value associations with sorted keys, ideal for tracking product inventory where we need to quickly look up stock levels by product ID.

6. Multimap for Customer Orders

multimap<string,Order> customerOrders;
for(const auto &order:orderHistory){
    customerOrders.insert({order.customerID,order});
}

Multimaps allow multiple values for the same key, making them perfect for tracking multiple orders from the same customer.

7. Unordered Containers for Fast Lookups

unordered_map<string,string> customerData = {
    {"C001","Alice"},
    {"C002","Bob"},
    {"C003","Max"},
    {"C004","ben"},
    {"C005","stokes"}
};

unordered_set<int> uniqueProductIDs;
for(const auto &product : products){
    uniqueProductIDs.insert(product.productID);
}

Unordered containers use hash tables for near-constant time lookups, making them excellent for customer data access and product ID validation.

Map vs. Unordered Map: When to Use Each

As noted in the code comments:

//map vs unordered_map
//map stores in sorted order whereas in unordered_map there is no guarantee of sorted data
//map uses binary search tree
//unordered_map uses hash table

This distinction is crucial for performance:

  • Use map when you need elements sorted by key or when you need to find elements within a range

  • Use unordered_map when you need the fastest possible lookups and don't care about element order

Struct vs Class: When to Use Each

In our online store implementation, we used structs for our data models (Product and Order). This wasn't a random choice - it follows C++ best practices. Let's explore when to use each:

Key Differences

The primary technical difference between structs and classes in C++ is the default access level:

  • In structs, members are public by default

  • In classes, members are private by default

Beyond this technical distinction, the choice between struct and class communicates design intent:

When to Use Structs

Structs are ideal for:

  • Simple data containers with little or no behaviour

  • When all data members can be publicly accessible

  • Value types representing a single logical entity (like coordinates)

  • Data Transfer Objects (DTOs) or passive data structures

Our Product and Order structs are perfect examples - they primarily store data with minimal behaviour.

When to Use Classes

Classes are better suited for:

  • Objects that need to maintain invariants (rules that keep data valid)

  • Data that requires encapsulation (hiding implementation details)

  • Types with significant behaviour beyond data storage

  • Objects with clear responsibilities and abstractions

  • When you need private members and controlled access

Code Example

If we were to expand our online store with more business logic, we might refactor to use classes:

// Using a class for cart management (has behavior + data)
class ShoppingCart {
private:
    vector<Order> items;
    double totalAmount;
    string customerID;

public:
    ShoppingCart(string custID) : customerID(custID), totalAmount(0.0) {}

    void addItem(Product product, int quantity);
    void removeItem(int productID);
    double calculateTotal();
    void checkout();
};

// Keeping struct for simple data representation
struct PaymentInfo {
    string paymentMethod;
    string cardNumber;
    string expiryDate;
};

Best Practice Rule of Thumb

A common guideline in C++ is:

  • Use structs when the main purpose is holding related data

  • Use classes when the type has behaviour that modifies or maintains its data

In our online store example, using structs for Product and Order was appropriate as they primarily represent data. If we were to add validation, business rules, or complex behaviour to these entities, a class might become more appropriate.

Managing Employees with C++ STL Algorithms and Lambda Expressions

In this blog, I'll explain how to use C++ Standard Template Library (STL) algorithms and lambda expressions to manage employee data efficiently. This approach demonstrates the power of modern C++ for data manipulation tasks.

Understanding the Employee Struct

struct Employee{
    int id;
    string name;
    double salary;
}

We use a struct (rather than a class) because this is a simple data container with no behavior and all public members. The struct contains an employee's ID, name, and salary - a perfect use case for structs over classes.

The code includes an interesting comment:

//Employee&  - reference
//&Employee  - address(pointer reference)

This distinguishes between:

  • Employee& - a reference to an Employee object

  • &Employee - the address (pointer) to an Employee

Displaying Employee Information

void displayEmployee(const Employee& emp){
    cout<<"ID : "<<emp.id<<" , Name : "<<emp.name<<" Salary : "<<emp.salary<<endl;
}

This function takes a const reference to an Employee (const Employee&), which prevents modification of the original object while avoiding unnecessary copying.

Vector Initialization

vector<Employee> employees = {
    {101,"harsha",1000000},
    {102,"ben",1000000},
    {103,"salt",1000000},
    {104,"rome",1000000},
    {105,"phil",1000000}
};

We create a vector of Employee objects using initializer lists - a convenient C++11 feature for creating and populating collections.

Sorting with Lambda Functions

sort(employees.begin(),employees.end(),[](const Employee& e1,const Employee& e2){
    return e1.salary>e2.salary;
});

This uses std::sort with a lambda function to sort employees by salary in descending order. The lambda:

  • Takes two Employee references as parameters

  • Returns true if the first employee's salary is greater than the second's

  • Uses the [] capture clause (empty in this case as we don't capture any variables from outside scope)

Displaying with for_each

for_each(employees.begin(),employees.end(),displayEmployee);

The std::for_each algorithm applies the displayEmployee function to each element in the vector, providing a cleaner alternative to traditional for loops.

Filtering High Earners

vector<Employee> highEarners;
copy_if(employees.begin(),employees.end(),back_inserter(highEarners),[](const Employee& e){
    return e.salary>40000;
});

This code:

  • Creates a new vector for high earners

  • Uses std::copy_if to selectively copy elements

  • Employs back_inserter to add elements to the destination vector

  • Uses a lambda to define the selection criteria (salary > 40000)

Calculating Total and Average Salary

double totalSalary = accumulate(employees.begin(),employees.end(),0.0,[](double sum,const Employee& e){
    return sum+e.salary;
});

double averageSalary = totalSalary/employees.size();

The std::accumulate algorithm:

  • Takes a starting value (0.0) and a binary operation (the lambda)

  • The lambda adds each employee's salary to the running sum

  • We then calculate the average by dividing by the number of employees

Finding the Highest Paid Employee

auto highestPaid = max_element(employees.begin(),employees.end(),[](const Employee& e1,const Employee& e2){
    return e1.salary<e2.salary;
})

std::max_element finds the largest element according to the comparison function:

  • The lambda compares salaries

  • Returns true if the first salary is less than the second

  • The algorithm returns an iterator to the highest paid employee

Conclusion

This code demonstrates several C++ STL algorithms and lambda expressions working together to manage employee data:

  1. std::sort for ordering employees by salary

  2. std::for_each for displaying employee information

  3. std::copy_if for filtering based on criteria

  4. std::accumulate for summing values

  5. std::max_element for finding maximum values

The use of lambda expressions makes the code more readable and concise by allowing us to define operation logic inline where it's used rather than creating separate named functions for each operation.

0
Subscribe to my newsletter

Read articles from BHEEMISETTY SAI HARSHA directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

BHEEMISETTY SAI HARSHA
BHEEMISETTY SAI HARSHA