C++ Class Construct  «Prev  Next»
Lesson 12 Program dissection
Objective Write const print and intersection Member Functions in C++23

Writing Const Print and Intersection Member Functions in C++23

Writing const member functions represents a fundamental skill in modern C++ programming, enabling developers to create robust, maintainable APIs that clearly distinguish read-only operations from state-modifying operations. This lesson explores practical implementation of const member functions through real-world examples, beginning with a salary calculation system that demonstrates the interplay between static and const member functions, then expanding to mathematical set operations where const correctness proves essential. Understanding how to design and implement const member functions, particularly for operations like printing output and computing intersections, forms the foundation for creating professional-quality C++ classes that leverage the compiler's type system to prevent bugs and express programmer intent clearly.

Salary Calculation Example with Static and Const

The salary calculation program demonstrates how static and const member functions work together in a practical application. This example illustrates key concepts: static members shared across all instances, const member functions that compute values without modifying state, and the proper use of the implicit this pointer in both contexts. By examining this complete working program, we can understand the mechanical details of how these features interact.
Apply, Filter, Sort
Complete salary calculation program demonstrating static and const member functions
// Calculate salary using static members
#include <iostream>

class Salary {
public:
   void init(int b) { 
      baseSalary = b; 
   }

   void calculateBonus(double percentage) {
      yourBonus = static_cast<int>(baseSalary * percentage);
   }

   // Static function: no this pointer, modifies shared state
   static void resetAllBonus(int amount) {
      allBonus = amount;
   }

   // Const function: has const this pointer, read-only
   int computeTotal() const {
      return baseSalary + yourBonus + allBonus;
   }

   // Additional const inspector functions
   int getBaseSalary() const { return baseSalary; }
   int getYourBonus() const { return yourBonus; }
   static int getAllBonus() { return allBonus; }

private:
   int baseSalary = 0;
   int yourBonus = 0;
   inline static int allBonus = 100;  // C++17 inline static
};

int main() {
   Salary w1, w2;

   w1.init(1000);
   w2.init(2000);
   w1.calculateBonus(0.2);
   w2.calculateBonus(0.15);
   
   // Static function call via class name (preferred)
   Salary::resetAllBonus(400);

   std::cout << "w1 total: " << w1.computeTotal() 
             << ", w2 total: " << w2.computeTotal() << '\n';

   return 0;
}


Dissecting the Salary Class

The Salary class contains three private data members, demonstrating different storage characteristics. The baseSalary and yourBonus members are instance-specific—each Salary object has its own copy. The allBonus member, declared as inline static in modern C++17 style, exists once for the entire class and is shared across all instances. This static member can exist independently of any Salary objects being created, representing class-level state rather than instance-level state.
class Salary {
private:
   int baseSalary = 0;      // Instance-specific
   int yourBonus = 0;       // Instance-specific
   inline static int allBonus = 100;  // Shared across all instances
};
The init() member function demonstrates basic member initialization. When called, it receives an implicit this pointer pointing to the object being initialized. The assignment baseSalary = b accesses the specific instance's baseSalary member through this pointer, effectively performing this->baseSalary = b. This pattern shows how ordinary member functions have access to instance-specific data.
The resetAllBonus() function demonstrates static member function characteristics. The static keyword appears before the return type in the declaration but is omitted in any out-of-line definition. This function receives no this pointer—it cannot access instance members like baseSalary or yourBonus. Instead, it operates only on static data, modifying the shared allBonus value that affects all Salary instances simultaneously.
// Static function: class-level operation
static void resetAllBonus(int amount) {
   allBonus = amount;  // OK: accessing static member
   // baseSalary = 0;  // ERROR: no this pointer, can't access instance data
}
The computeTotal() function exemplifies const member functions. The const keyword appears after the parameter list, modifying the type of the implicit this pointer from Salary* const to const Salary* const. This change prevents the function from modifying any non-mutable data members, making the code more robust by allowing the compiler to enforce immutability. The function can read all members—both instance and static—but cannot modify them, making it safe to call on const objects.
// Const function: read-only operation
int computeTotal() const {
   // Implicit: const Salary* const this
   return baseSalary + yourBonus + allBonus;  // OK: reading
   // baseSalary = 0;  // ERROR: cannot modify through const this
}
Static member functions can be invoked using the scope resolution operator with the class name: Salary::resetAllBonus(400). While technically you can call static functions through object instances like w1.resetAllBonus(400), this practice misleads readers into thinking the function depends on that particular object. Always prefer calling static functions through the class name to clearly communicate that you're invoking a class-level operation, not an instance-specific one.

Implementing Const Print Functions

class Employee {
private:
   std::string name;
   int id;
   double salary;

public:
   Employee(const std::string& n, int i, double s)
      : name(n), id(i), salary(s) {}

   // Simple const print to cout
   void print() const {
      std::cout << "Employee: " << name 
                << " (ID: " << id 
                << ", Salary: $" << salary << ")\n";
   }

   // Flexible const print accepting ostream reference
   void print(std::ostream& os) const {
      os << "Employee: " << name 
         << " (ID: " << id 
         << ", Salary: $" << salary << ")";
   }

   // Alternative: return string representation
   std::string toString() const {
      std::ostringstream oss;
      oss << "Employee: " << name 
          << " (ID: " << id 
          << ", Salary: $" << salary << ")";
      return oss.str();
   }

   // Friend function for stream operator (not a member)
   friend std::ostream& operator<<(std::ostream& os, const Employee& emp) {
      emp.print(os);
      return os;
   }
};

void demonstratePrintMethods() {
   Employee emp("Alice Johnson", 12345, 75000.0);

   // Method 1: Direct print
   emp.print();

   // Method 2: Print to specific stream
   std::ofstream logFile("employees.log");
   emp.print(logFile);
   logFile << '\n';

   // Method 3: Get string representation
   std::string empStr = emp.toString();
   std::cout << empStr << '\n';

   // Method 4: Stream operator
   std::cout << emp << '\n';
}


Mathematical Set Class with Const Operations

A mathematical set provides an excellent example for implementing const member functions, particularly for operations like printing the set contents and computing intersections. A set represents an unordered collection of unique elements, and many set operations—including printing and intersection—naturally preserve the original sets without modification, making them perfect candidates for const member functions.
template<typename T>
class Set {
private:
   std::set<T> elements;

public:
   Set() = default;

   // Constructor from initializer list
   Set(std::initializer_list<T> init) : elements(init) {}

   // Non-const mutators
   void insert(const T& element) {
      elements.insert(element);
   }

   void remove(const T& element) {
      elements.erase(element);
   }

   void clear() {
      elements.clear();
   }

   // Const inspectors
   size_t size() const {
      return elements.size();
   }

   bool empty() const {
      return elements.empty();
   }

   bool contains(const T& element) const {
      return elements.find(element) != elements.end();
   }

   // Const print function
   void print() const {
      std::cout << "{ ";
      bool first = true;
      for (const auto& elem : elements) {
         if (!first) std::cout << ", ";
         std::cout << elem;
         first = false;
      }
      std::cout << " }";
   }

   // Const print with custom output stream
   void print(std::ostream& os) const {
      os << "{ ";
      bool first = true;
      for (const auto& elem : elements) {
         if (!first) os << ", ";
         os << elem;
         first = false;
      }
      os << " }";
   }

   // Const intersection operation
   Set intersection(const Set& other) const {
      Set result;
      for (const auto& elem : elements) {
         if (other.contains(elem)) {
            result.insert(elem);
         }
      }
      return result;
   }

   // Additional const set operations
   Set setUnion(const Set& other) const {
      Set result = *this;  // Copy current set
      for (const auto& elem : other.elements) {
         result.insert(elem);
      }
      return result;
   }

   Set difference(const Set& other) const {
      Set result;
      for (const auto& elem : elements) {
         if (!other.contains(elem)) {
            result.insert(elem);
         }
      }
      return result;
   }

   bool isSubsetOf(const Set& other) const {
      for (const auto& elem : elements) {
         if (!other.contains(elem)) {
            return false;
         }
      }
      return true;
   }

   // Stream operator (friend function)
   friend std::ostream& operator<<(std::ostream& os, const Set& s) {
      s.print(os);
      return os;
   }
};


Using the Set Class with Const Operations

The Set class demonstrates how const member functions enable working with both const and non-const objects while guaranteeing that inspection operations don't modify the original data. The print and intersection functions exemplify this principle—they examine set contents and perform computations without altering the sets themselves, making them safe to call on const-qualified objects.
void demonstrateSetOperations() {
   // Create some sets
   Set<int> setA{1, 2, 3, 4, 5};
   Set<int> setB{4, 5, 6, 7, 8};
   const Set<int> setC{2, 4, 6, 8, 10};

   // Print sets (const operation)
   std::cout << "Set A: ";
   setA.print();
   std::cout << '\n';

   std::cout << "Set B: " << setB << '\n';
   std::cout << "Set C: " << setC << '\n';

   // Intersection (const operation)
   Set<int> intersectionAB = setA.intersection(setB);
   std::cout << "A ∩ B: " << intersectionAB << '\n';

   // Can call const functions on const object
   Set<int> intersectionAC = setA.intersection(setC);
   std::cout << "A ∩ C: " << intersectionAC << '\n';

   // Other const operations
   Set<int> unionAB = setA.setUnion(setB);
   std::cout << "A ∪ B: " << unionAB << '\n';

   Set<int> diffAB = setA.difference(setB);
   std::cout << "A - B: " << diffAB << '\n';

   // Boolean operations (const)
   bool isSubset = intersectionAB.isSubsetOf(setA);
   std::cout << "(A ∩ B) ⊆ A: " 
             << (isSubset ? "true" : "false") << '\n';

   // Demonstrate const correctness
   setC.print();  // OK: const function on const object
   // setC.insert(12);  // ERROR: cannot call non-const on const object
}

Advanced Const Print Patterns

Professional C++ code often employs sophisticated printing strategies that leverage const correctness. These patterns include formatted output with configurable delimiters, pretty-printing with indentation for nested structures, debugging output that includes internal state, and serialization to various formats. All these printing operations should be const since they observe rather than modify object state.
template<typename T>
class Matrix {
private:
   std::vector<std::vector<T>> data;
   size_t rows, cols;

public:
   Matrix(size_t r, size_t c) 
      : rows(r), cols(c), data(r, std::vector<T>(c)) {}

   // Basic const print
   void print() const {
      for (size_t i = 0; i < rows; ++i) {
         for (size_t j = 0; j < cols; ++j) {
            std::cout << std::setw(8) << data[i][j];
         }
         std::cout << '\n';
      }
   }

   // Formatted const print with options
   void printFormatted(std::ostream& os, 
                      int width = 8,
                      int precision = 2) const {
      os << std::fixed << std::setprecision(precision);
      for (size_t i = 0; i < rows; ++i) {
         os << "[";
         for (size_t j = 0; j < cols; ++j) {
            os << std::setw(width) << data[i][j];
            if (j < cols - 1) os << ", ";
         }
         os << "]\n";
      }
   }

   // Debug print with metadata (const)
   void printDebug() const {
      std::cout << "Matrix(" << rows << "x" << cols << "):\n";
      print();
      std::cout << "Memory: " << (rows * cols * sizeof(T)) 
                << " bytes\n";
   }

   // Compact single-line print (const)
   void printCompact(std::ostream& os) const {
      os << "[";
      for (size_t i = 0; i < rows; ++i) {
         os << "[";
         for (size_t j = 0; j < cols; ++j) {
            os << data[i][j];
            if (j < cols - 1) os << ",";
         }
         os << "]";
         if (i < rows - 1) os << ",";
      }
      os << "]";
   }

   T& at(size_t i, size_t j) { return data[i][j]; }
   const T& at(size_t i, size_t j) const { return data[i][j]; }
};


Efficient Intersection Implementation

Implementing set intersection as a const member function requires careful attention to both correctness and efficiency. The operation must examine both sets without modifying either, returning a new set containing only elements present in both. For large sets, algorithmic efficiency matters—using sorted containers like std::set provides logarithmic lookup time, while hash-based containers like std::unordered_set offer average constant-time lookups.
template<typename T>
class OptimizedSet {
private:
   std::unordered_set<T> elements;

public:
   OptimizedSet() = default;
   OptimizedSet(std::initializer_list<T> init) : elements(init) {}

   void insert(const T& elem) { elements.insert(elem); }

   bool contains(const T& elem) const {
      return elements.find(elem) != elements.end();
   }

   size_t size() const { return elements.size(); }

   // Optimized intersection: iterate smaller set
   OptimizedSet intersection(const OptimizedSet& other) const {
      OptimizedSet result;
      
      // Iterate through smaller set for efficiency
      const auto& smaller = (size() < other.size()) ? *this : other;
      const auto& larger = (size() < other.size()) ? other : *this;

      for (const auto& elem : smaller.elements) {
         if (larger.contains(elem)) {
            result.insert(elem);
         }
      }

      return result;
   }

   // In-place intersection using STL algorithm
   OptimizedSet intersectionSTL(const OptimizedSet& other) const {
      // Convert to sorted vectors for std::set_intersection
      std::vector<T> vec1(elements.begin(), elements.end());
      std::vector<T> vec2(other.elements.begin(), other.elements.end());
      
      std::sort(vec1.begin(), vec1.end());
      std::sort(vec2.begin(), vec2.end());

      std::vector<T> result;
      std::set_intersection(
         vec1.begin(), vec1.end(),
         vec2.begin(), vec2.end(),
         std::back_inserter(result)
      );

      OptimizedSet resultSet;
      for (const auto& elem : result) {
         resultSet.insert(elem);
      }
      return resultSet;
   }

   // Print function (const)
   void print(std::ostream& os = std::cout) const {
      os << "{ ";
      bool first = true;
      for (const auto& elem : elements) {
         if (!first) os << ", ";
         os << elem;
         first = false;
      }
      os << " }";
   }
};

Orthogonal Class Design Principles

Orthogonal class design involves creating a minimal set of independent operations that provide complete functionality without redundancy. Like a radio with separate band selection and tuning controls rather than separate tuners for each band, orthogonal design reduces complexity while maintaining full capability. When designing classes with const member functions, orthogonality means providing inspection operations that are truly independent of mutation operations, allowing users to combine them in any order without unexpected interactions.
Class Carton inherits from class Box, class FoodCarton inherits from ClassCarton.
Class hierarchy demonstrating orthogonal design: Carton inherits from Box, FoodCarton inherits from Carton

In an orthogonal class design, const and non-const operations should be clearly separated. Const operations (inspectors) examine state without modifying it, while non-const operations (mutators) change state. This separation creates predictable behavior—calling a sequence of const operations in any order produces consistent results, and the presence of const operations doesn't affect how mutators work. The design favors client convenience while maintaining implementation efficiency.
// Example of orthogonal design with clear separation
class BankAccount {
private:
   std::string accountNumber;
   double balance;
   std::vector<std::string> transactionLog;

public:
   BankAccount(const std::string& acct, double initial)
      : accountNumber(acct), balance(initial) {}

   // INSPECTORS: Const operations (orthogonal - independent)
   double getBalance() const { return balance; }
   
   const std::string& getAccountNumber() const { 
      return accountNumber; 
   }

   size_t getTransactionCount() const {
      return transactionLog.size();
   }

   bool canWithdraw(double amount) const {
      return amount > 0 && amount <= balance;
   }

   void printStatement() const {
      std::cout << "Account: " << accountNumber << '\n';
      std::cout << "Balance: $" << balance << '\n';
      std::cout << "Transactions: " << transactionLog.size() << '\n';
   }

   // MUTATORS: Non-const operations (orthogonal to inspectors)
   bool deposit(double amount) {
      if (amount > 0) {
         balance += amount;
         transactionLog.push_back("Deposit: $" + std::to_string(amount));
         return true;
      }
      return false;
   }

   bool withdraw(double amount) {
      if (canWithdraw(amount)) {  // Uses const inspector
         balance -= amount;
         transactionLog.push_back("Withdrawal: $" + std::to_string(amount));
         return true;
      }
      return false;
   }
};

Orthogonal Persistence and Data Longevity

Orthogonal persistence refers to treating data uniformly regardless of how long it persists in storage. A well-designed class with const member functions supports orthogonal persistence by ensuring inspection operations work identically whether data resides in memory, on disk, or in a database. Const member functions facilitate this by guaranteeing they don't modify state, making them safe to call regardless of the underlying storage mechanism or data lifetime.
// Example supporting orthogonal persistence
class Document {
private:
   std::string content;
   std::string filename;
   mutable bool cached = false;
   mutable std::string cachedMetadata;

public:
   explicit Document(const std::string& file) : filename(file) {
      loadFromFile();
   }

   // Const operations work regardless of persistence state
   std::string getContent() const { return content; }
   
   size_t getSize() const { return content.size(); }

   // Lazy computation with caching (const with mutable)
   std::string getMetadata() const {
      if (!cached) {
         cachedMetadata = "File: " + filename + 
                         ", Size: " + std::to_string(content.size());
         cached = true;
      }
      return cachedMetadata;
   }

   void print() const {
      std::cout << getMetadata() << '\n';
      std::cout << content << '\n';
   }

   // Non-const operations modify state
   void setContent(const std::string& newContent) {
      content = newContent;
      cached = false;  // Invalidate cache
   }

   void saveToFile() {
      std::ofstream file(filename);
      file << content;
   }

private:
   void loadFromFile() {
      std::ifstream file(filename);
      content = std::string(
         (std::istreambuf_iterator<char>(file)),
         std::istreambuf_iterator<char>()
      );
   }
};


Best Practices for Const Member Functions

Writing effective const member functions requires following established patterns. Mark every member function const unless it genuinely needs to modify object state. Use mutable sparingly for caching and synchronization, not as a workaround for poor design. Provide both const and non-const overloads for functions returning references to internal data. Design print functions to accept std::ostream references for flexibility. Implement mathematical operations like intersection as const functions returning new objects rather than modifying operands. Document thread-safety guarantees for const functions that use mutable members. Test const-correctness by creating const instances and verifying the compiler accepts your const function calls.

Conclusion

Writing const member functions for operations like printing and computing set intersections represents fundamental C++ craftsmanship that distinguishes professional code from amateur implementations. Through examining practical examples—from salary calculations demonstrating static and const interplay to mathematical sets showcasing comprehensive const operation design—we've explored how const-correctness enables robust, maintainable APIs that leverage compiler guarantees. The principles of orthogonal design, where inspection and mutation operations remain independent and composable, create classes that are easier to understand, test, and extend. Whether implementing simple print functions that display object state or complex algorithms like set intersection that compute new values from existing data, the discipline of const-correctness ensures your classes communicate intent clearly, prevent accidental modifications, and enable optimizations. By mastering these patterns and applying them consistently throughout your codebase, you create C++ software that balances expressiveness with safety, providing users with predictable, efficient interfaces that stand the test of time.

Const Member Function - Exercise

Click the exercise link below to add const print and intersection member functions to a class that implements a mathematical set.
Const Member Function - Exercise

SEMrush Software