Pure Virtual Functions

Learn how to create interfaces and abstract classes using pure virtual functions
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

We’ve previously seen the benefits of polymorphism.

When we have a variable or function parameter that is a pointer or a reference to a type, objects of any class that inherit from that type can be used.

In the following example, we have a Taunt function that accepts a reference to a Character. We can pass an object of a type of Orc to this function.

This works because Orc inherits from Character, therefore all Orcs are also Characters:

class Character {};

class Orc : public Character {};

void Taunt(Character& Taunter) {
  // ...
};

int main() {
  Orc Basher;
  Taunt(Basher);
}

We’ve also seen how we can use virtual functions to override the behavior of those base implementations. This allows our function to behave differently based on the specific subtype of Character it receives. This is the essence of polymorphism:

#include <iostream>

class Character {
public:
  virtual std::string GetTaunt() {
    return "???";
  }
};

class Orc : public Character {
public:
  std::string GetTaunt() override {
    return "Come get some!";
  }
};

void Taunt(Character& Taunter) {
  std::cout << Taunter.GetTaunt();
};

int main() {
  Orc Basher;
  Taunt(Basher);
}
Come get some!

This allows our code to manage complexity elegantly. We could have hundreds of different character subtypes, each with unique behaviors. But, that is all hidden away within their respective class code - our Taunt() function never needs to change.

A common problem arises with this design: the function we want to call needs to exist on the base class, but it’s not always obvious what that base function should do, or what value it should return.

class Character {
public:
  virtual std::string GetTaunt() {
    return "???";
  }
};

The Character class only really exists as a way to create a standardized interface among all its subtypes. We never expect to create basic Character objects - we’re always going to create more specific objects.

So, in this scenario, GetTaunt() on our Character class can be a pure virtual function.

Pure Virtual Functions

To create a pure virtual function, we assign it a value of 0:

class Character {
public:
  virtual std::string GetTaunt() = 0;
};

After making this change, Character is now an abstract class. Abstract classes have two properties:

  1. It is no longer possible to create objects from this class - Character objects will now need to be created from more specific subclasses
  2. If a subclass of Character does not override this function to provide an implementation, that class will also be abstract
#include <string>

// Abstract - GetTaunt() is pure virtual
class Character {
public:
  virtual std::string GetTaunt() = 0;
};

// Abstract - GetTaunt() is pure virtual
class Fish : public Character {};

// Not Abstract - GetTaunt() is implemented
class Orc : public Character {
public:
  std::string GetTaunt() override {
    return "Come get some!";
  }
};

int main() {
  // We can't create objects from abstract types
  // These statements will cause compiler errors
  Character Henry;
  Fish Flappy;

  // We can only instantiate non-abstract types
  Orc Basher;
}
error C2259: 'Character': cannot instantiate abstract class
error C2259: 'Fish': cannot instantiate abstract class

Typically, classes that are not abstract are referred to as "concrete". So, in the above example, Orc is a "concrete class".

Optional Implementations

There is an additional technique that allows pure virtual functions to have an optional implementation. It looks like this:

#include <string>

class Character {
public:
  virtual std::string GetTaunt() = 0;
};

std::string Character::GetTaunt() {
  return "Default taunt";
}

Here, Character is still an abstract class, but, for classes that inherit from Character, a default implementation of this function is available.

If any child classes want to use that implementation, they need to explicitly call it:

class Human : public Character {
public:
  std::string GetTaunt() override {
    return Character::GetTaunt();
  }
};

Typically, the motivation for doing this is that we expect most subclasses will need to override this function. If it’s not overridden, we want the class to explicitly confirm they’re happy with the default implementation.

This mitigates scenarios where a developer wasn’t aware that something normally needs to be overridden, or they simply forgot to do so.

Interfaces

Many other object-oriented programming languages have the concept of interfaces. Interfaces specify a set of requirements for an object but do not provide any code to implement those requirements.

C++ does not have a formal implementation of interfaces, but the same behavior can be achieved using a combination of abstract classes and multiple inheritance.

A class that has no variables, and only pure-virtual functions, is effectively an interface. By convention, classes that are intended to be used as interfaces use some agreed naming convention, such as prepending an I at the start of their name:

class ITaunter {
public:
  virtual std::string GetTaunt() = 0;
};

This gives us an alternative way to implement our design. Now, our GetTaunt() function no longer needs to accept a Character&, it can instead accept an ITaunter&:

void Taunt(ITaunter& Taunter) {
  std::cout << Taunter.GetTaunt();
};

This frees us from needing to define GetTaunt() in our Character base class. It also gives us more flexibility in our design. All characters can still be Character objects, but now, each class can decide whether or not it also wants to be an ITaunter.

It does that by inheriting from ITaunter, and implementing the ITaunter requirements - that is, implementing the functions that are defined as pure-virtuals on the ITaunter class.

If a class does that, its objects then also have the ITaunter type, and can be passed to any functions that accept that type. Below, both Orc and Fish objects are Characters, but Orc is additionally an ITaunter:

#include <iostream>

class Character {};
class Fish : public Character {};

class ITaunter {
public:
  virtual std::string GetTaunt() = 0;
};

class Orc : public Character, public ITaunter {
public:
  std::string GetTaunt() override {
    return "Come get some!";
  }
};

void Taunt(ITaunter& Taunter) {
  std::cout << Taunter.GetTaunt();
};

int main() {
  // A Character that can taunt
  Orc Basher;
  Taunt(Basher); 

  // A Character that cannot taunt
  Fish Flappy;
  Taunt(Flappy); 
}
error C2664: 'void Taunt(ITaunter &)': cannot convert argument 1 from 'Fish' to 'ITaunter &'

Summary

In this lesson, we explored pure virtual functions, abstract classes, and interfaces, showcasing how these features give us more flexibility in our designs.

Main Points Learned:

  • Pure virtual functions are declared with = 0 and make a class abstract, preventing it from being instantiated directly.
  • Abstract classes serve as a blueprint for derived classes, requiring them to implement the pure virtual functions.
  • A pure virtual function can have an implementation that derived classes can optionally use.
  • Concrete classes are non-abstract and provide implementations for all inherited pure virtual functions, making them instantiable.
  • C++ uses abstract classes with pure virtual functions to achieve interface-like behavior.
  • Multiple inheritance allows a class to implement multiple interfaces by inheriting from abstract classes.

Was this lesson useful?

Next Lesson

List, Aggregate, and Designated Initialization

A quick guide to creating objects using lists, including std::initializer_list, aggregate and designated initialization
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Pure Virtual Functions

Learn how to create interfaces and abstract classes using pure virtual functions

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 125 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

List, Aggregate, and Designated Initialization

A quick guide to creating objects using lists, including std::initializer_list, aggregate and designated initialization
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved