An abstract class is a class that is intentionally incomplete. It declares operations that derived classes must fill in, while still being usable as a common type for those derived classes. This lesson focuses on why abstract classes exist, when to use one, and how they shape the design of an E-Commerce system.
A class becomes abstract the moment it contains at least one pure virtual function. The previous section covered the = 0 syntax, so the focus here is on the consequence, not the spelling. The consequence is direct: you cannot instantiate the class directly, but you can still use it as a base, as a pointer type, and as a reference type.
A small PaymentProcessor example grounds the idea.
PaymentProcessor is abstract. CreditCardProcessor is concrete because it provides bodies for both pure virtuals. If you tried PaymentProcessor p;, the compiler would refuse and tell you the class is abstract.
Abstract classes solve two problems at once.
The first is contract enforcement. When you declare a function pure virtual in the base, every concrete derived class is forced to implement it. The compiler will not let a derived class forget. That guarantee removes a whole category of runtime surprises where a method exists but does nothing useful.
The second is modeling incomplete concepts. "Payment processor" is not a thing you can build on its own. You can build a credit card processor, a PayPal processor, a bank transfer processor. The abstract class captures what they all share without pretending the shared idea is something you can construct.
Put differently, a concrete class answers "what is this thing?" and an abstract class answers "what role does this thing play?".
The same idea as a hierarchy:
The arrows go from base to derived. The base sits at the top as an abstract contract. The leaves are the only classes you would create instances of.
Abstract classes are forbidden as values, but they are valid as pointers and references. That is the point.
The variable concrete is the actual object. ref and ptr are handles that look at it through the abstract type. The virtual dispatch handles the rest, so ref.charge ends up running PayPalProcessor::charge because that is the actual type of the object behind the reference.
This is what makes the abstract class useful as a parameter type, a return type, and the element type of a polymorphic container.
Once you can take a reference or pointer to an abstract type, you can write functions that accept any concrete derivation without naming them.
checkout does not know anything about credit cards or bank transfers. It only knows it has something that can be charged and that knows its own name. That is the contract PaymentProcessor enforces, and the function works with any class that signs that contract.
The parameter is PaymentProcessor&, not PaymentProcessor. If you tried to pass by value, the compiler would reject it because you cannot have a value of an abstract type. By-value would also slice the object, throwing away the derived part, a problem covered back in the polymorphism section.
Passing by reference (or pointer) to the abstract base has negligible cost. Each virtual call adds one extra pointer dereference through the vtable, which is small compared to anything that does real work like a network call or a database write.
An abstract class is not limited to a list of pure virtuals. It can have data members, regular member functions, and constructors. The abstractness comes from the unimplemented operations, not from a lack of everything else.
DiscountStrategy holds the promo code as a data member, has a constructor that initializes it, and exposes a regular promoCode() getter plus a describe() function with a real body. Every derived class inherits all of that. Only applyTo is left for them to implement.
Derived constructors call the base constructor through the member initializer list, exactly like with any other inheritance. The base's constructor runs first, sets up its data members, and then the derived constructor runs. The abstractness changes nothing about the construction order.
One more example reinforces the pattern. Different shipping methods compute different costs and quote different delivery times, but the order code should not care which one is in use.
quoteShipping is written once and works for both methods. If a new OvernightShipping shows up next year, it derives from ShippingMethod, implements the three pure virtuals, and quoteShipping works with it without changing a line.
The order code on the left depends only on the abstract type in the middle. New concrete shipping methods plug in on the right without touching the order code at all.
You cannot return an abstract type by value (same reason you cannot pass by value). You can return a pointer or a smart pointer to one. This is the shape of a factory function, covered in detail later. The relevant piece for this lesson is that abstract classes are first-class return types when wrapped in indirection.
makeProcessor returns a std::unique_ptr<PaymentProcessor>. The caller gets a smart pointer to the abstract base, never knowing or caring which concrete type came back. That hidden choice is what abstraction buys you. The caller writes p1->charge(19.99) and the runtime picks the correct charge based on the actual type the pointer holds.
std::make_unique performs one heap allocation per object. For a payment processor created once per checkout this is negligible. For millions per second, pooling or reuse would be preferable.
A few notes on this pattern. Returning std::unique_ptr<PaymentProcessor> works because the smart pointer holds a pointer internally, and pointers to abstract types are valid. Returning PaymentProcessor directly would not compile. Returning PaymentProcessor* (a raw pointer) would compile but force the caller to remember to delete it, which is the kind of bug the smart pointer prevents.
A later lesson covers factory functions in detail. For now the takeaway is that abstract types are usable as return types when wrapped in some form of indirection.
A single function parameter can take any concrete derivation. So can a single container element. A std::vector<std::unique_ptr<PaymentProcessor>> is a list of "anything that is a payment processor," and the code that walks it does not need to know what is inside.
The vector stores unique_ptr<PaymentProcessor> elements. Each pointer points to a different concrete type, but the loop treats them identically. Virtual dispatch picks the correct charge and the correct name for each one.
The runtime picture:
Each element is a smart pointer of the same static type. Each one points to a different concrete object on the heap. That is polymorphism, end to end.
A few patterns come up when working with abstract classes.
The first is trying to pass or store by value. Anything that needs to hold an abstract type has to hold it through a pointer or reference. PaymentProcessor p, std::vector<PaymentProcessor>, and PaymentProcessor make() all fail to compile.
The second is forgetting the virtual destructor. If you delete a derived object through a pointer to the abstract base, and the base does not declare its destructor virtual, only the base destructor runs. Resources held by the derived part leak. The rule: abstract base classes that are ever deleted polymorphically need virtual ~Base() = default;.
The third is assuming abstract means "no data". As the DiscountStrategy example showed, an abstract class can carry data, regular methods, and constructors. It is only abstract because at least one operation is left unimplemented, not because it lacks state.
The fourth is not marking the override. The override keyword is not required, but using it catches mistakes where a derived function does not actually match the base signature. Without override, a typo in the signature creates a brand-new function on the derived class and the pure virtual stays unimplemented, which leaves the derived class abstract by accident.
Abstract classes are not the answer to every problem. Use one when all of the following are true.
You have a clear "is-a" relationship between a general concept and several specific variations. Payment processors, shipping methods, and discount strategies all fit this. A "thing that has a vector inside it" does not.
The code that uses the concept does not need to know which specific variation it is dealing with. The checkout function does not care which payment processor; the order code does not care which shipping method. If your caller is constantly checking "is this a CreditCard or a PayPal?", the abstraction is not pulling its weight.
You expect new variations over time, or you want existing variations to be swappable. The cost of an abstract base pays off when the alternative is editing the caller every time a new option appears.
If none of those apply, a plain class or a free function is probably the better fit. Abstraction has a cost. Every layer adds one more thing for a reader to chase, and every virtual call adds one indirection. Use it where it pays for itself.
10 quizzes