C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
34: Interfaces via Abstract Base Classes
I've been thinking about how we handle logging in our project. Right now, we're just using std::cout everywhere, which is fine for a prototype, but it's a nightmare for production. We need a way to swap where our logs go—maybe a file, maybe a database, or maybe a cloud service—without changing every single line of code that calls log().
The problem with generic loggers
My first instinct was to just make a base class. Something simple. Let's see what happens when I try to set this up the "obvious" way.
class Logger {
public:
void log(const std::string& message) {
// What goes here? I don't actually know how to log
// until I know WHERE I'm logging to.
}
};
class ConsoleLogger : public Logger {
public:
void log(const std::string& message) override {
std::cout << "[Console]: " << message << std::endl;
}
};
Wait, I just noticed a problem. If I write this, the compiler lets me do this: Logger myLogger; myLogger.log("Hello");. But a generic Logger doesn't actually do anything. It's a useless object. It's a conceptual "idea" of a logger, not a functional tool. In a large codebase, someone will eventually instantiate the base class by mistake, and your logs will just vanish into a void because the base log() method is empty.
Stopping the "Empty" Implementation
I don't want to provide a default implementation because there is no such thing as a "default" way to log. I want to force whoever creates a new logger to define exactly how log() works. Let's try making it a pure virtual function. I'll add = 0 to the end of the declaration.
class Logger {
public:
virtual ~Logger() {} // Always remember the virtual destructor for base classes!
virtual void log(const std::string& message) = 0;
};
Now, let's see what happens if I try to create a generic logger again: Logger myLogger;. The compiler immediately throws a fit. It tells me that Logger is an "abstract class" and cannot be instantiated. This is exactly what I wanted. The compiler is now acting as my quality control, ensuring that no one can ever create a "half-baked" logger.
Letting the Compiler Enforce the Contract
Now, here is where it gets interesting. If I create a FileLogger but I forget to implement the log() method, I'd expect it to work just like the base class. Let's test that theory.
class FileLogger : public Logger {
// I'm "forgetting" to implement log() here
};
The compiler catches me again. It refuses to build FileLogger because it's still abstract. By using a pure virtual function, I've created an Interface. I'm not telling the derived classes how to log; I'm telling them what they must be able to do if they want to be considered a Logger.
Tying it all together with a Pointer
The real payoff happens when we use these interfaces in our actual logic. I don't want my UserAuth class to know if it's talking to a console or a file; it just needs to know that it has something that can log.
void processLogin(Logger* logger) {
// This function doesn't care about the specific implementation
logger->log("User attempted login...");
}
int main() {
ConsoleLogger cLog;
FileLogger fLog;
processLogin(&cLog); // Works!
processLogin(&fLog); // Also works!
}
This is the essence of an interface via an abstract base class. We've decoupled the usage of the logger from the implementation of the logger. If we decide next week to send logs to a Slack channel, we just create a SlackLogger that implements the interface, and processLogin doesn't have to change a single character of code. It's clean, it's safe, and the compiler does the heavy lifting of making sure we didn't forget any methods.
📋 Practical Task
Build a Multi-Provider Payment System
You are building a checkout system that needs to support multiple payment methods (e.g., Credit Card, PayPal, Bitcoin). You don't want the checkout logic to be rewritten every time a new payment provider is added.
Your Task:
- Create an abstract base class named
PaymentProcessor. - Define a pure virtual function
processPayment(double amount)that returns abool. - Implement two derived classes:
CreditCardProcessorandPayPalProcessor. Each should print a unique message (e.g., "Processing credit card payment of $X") and returntrue. - Write a function called
executeTransaction(PaymentProcessor* processor, double amount)that takes a pointer to the interface and calls theprocessPaymentmethod. - In your
main()function, instantiate both processors and pass them intoexecuteTransactionto demonstrate that the same function can handle different payment types.
There are no comments for now.