C++ Virtual Functions: Polymorphism, override, and Pure

Key takeaways

Virtual functions in C++: dynamic dispatch, virtual vs non-virtual, pure virtual and abstract classes, virtual destructors, vtables, and slicing pitfalls.

What are virtual functions?

This article explains how virtual functions enable C++ polymorphism, why the right override runs at runtime, and how to use override, pure virtual functions, and virtual destructors in real code, with three worked designs (a composite file tree, payment strategies, loggers). The machinery underneath is only summarized here; C++ VTable Explained goes into object layout and call cost.

C++ resolves most function calls statically, at compile time, based purely on the declared (static) type of the expression you called through. That is fast and predictable, but it breaks down the moment you want to write code against a base-class interface and have it automatically pick the correct derived-class behavior for whatever concrete object happens to be behind a pointer or reference at runtime. That is exactly the problem virtual functions solve: a function marked virtual is resolved by the object’s dynamic type — the actual most-derived type it was constructed as — not by the type of the pointer or reference you’re holding. This single mechanism is what makes runtime polymorphism possible in C++, and it underpins nearly every plugin system, strategy pattern, visitor pattern, and abstract interface you’ll encounter in production C++ code.

The classic illustration is a base class Animal with derived classes Dog and Cat. Code that only knows about Animal* can still call the right speak() for whichever concrete animal it’s actually pointing at:

class Animal {
public:
    virtual void speak() {
        cout << "Animal sound" << endl;
    }
};
class Dog : public Animal {
public:
    void speak() override {
        cout << "Woof!" << endl;
    }
};
class Cat : public Animal {
public:
    void speak() override {
        cout << "Meow!" << endl;
    }
};
int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();
    
    animal1->speak();  // Woof!
    animal2->speak();  // Meow!
    
    delete animal1;
    delete animal2;
}

Notice what makes this work: animal1 and animal2 are both declared as Animal*. If speak() were an ordinary (non-virtual) member function, the compiler would bake the call to Animal::speak directly into the machine code at the call site, because that’s all the compiler statically knows. Marking speak() virtual tells the compiler to defer that decision to runtime — the actual object decides which override answers the call. This is the mechanism behind “program to an interface, not an implementation”: calling code depends only on the Animal interface, while new animal types can be added later without touching a single line of the code that calls speak().

virtual vs non-virtual

The distinction between virtual and non-virtual member functions is really the distinction between static binding and dynamic binding, and it’s one of the most common sources of subtle bugs for developers coming from languages where every method is implicitly virtual (Java, C#, Python). In C++, non-virtual is the default — you have to opt in to dynamic dispatch explicitly. That default exists for a reason: non-virtual calls can be resolved and often inlined at compile time, which is faster and gives the compiler more optimization freedom. But it also means that if you call a non-virtual function through a base-class pointer, you always get the base class’s version, no matter what the actual object is — even if a derived class defines a function with the exact same name and signature.

class Base {
public:
    void nonVirtual() {
        cout << "Base::nonVirtual" << endl;
    }
    
    virtual void virtualFunc() {
        cout << "Base::virtualFunc" << endl;
    }
};
class Derived : public Base {
public:
    void nonVirtual() {
        cout << "Derived::nonVirtual" << endl;
    }
    
    void virtualFunc() override {
        cout << "Derived::virtualFunc" << endl;
    }
};
int main() {
    Base* ptr = new Derived();
    
    ptr->nonVirtual();   // Base::nonVirtual (static binding)
    ptr->virtualFunc();  // Derived::virtualFunc (dynamic binding)
    
    delete ptr;
}

The output line ptr->nonVirtual(); // Base::nonVirtual (static binding) surprises a lot of developers the first time they see it, because ptr is holding a Derived object — intuitively you’d expect Derived::nonVirtual to run. But the compiler decided which function to call the moment it compiled ptr->nonVirtual(), using only the declared type of ptr (Base*). This is called name hiding: Derived::nonVirtual doesn’t override Base::nonVirtual at all (since it isn’t virtual, there’s no override relationship), it simply hides the base class name within the scope of Derived. If you only ever access the object through a Derived* or Derived&, you’d get Derived::nonVirtual; through a Base*, you always get Base::nonVirtual. This is exactly why library and framework authors mark any method intended for polymorphic dispatch virtual deliberately, rather than assuming C++ will “figure it out” — unlike Java, nothing happens automatically here.

Pure virtual functions

A pure virtual function — declared with the = 0 syntax — goes one step further than an ordinary virtual function: it has no implementation requirement in the base class at all (though it may still optionally provide one that derived classes can call explicitly via Base::func()), and any class that declares at least one pure virtual function becomes an abstract class. Abstract classes cannot be instantiated directly; the compiler enforces that only concrete classes that override every pure virtual member can be constructed. This turns a design intention — “every shape must know how to compute its own area and draw itself” — into a compile-time guarantee rather than a runtime convention or a comment. It’s the closest C++ gets to a Java interface or a Rust trait, and it’s the standard vocabulary for expressing “here is a contract; I don’t care how you fulfill it.”

class Shape {
public:
    virtual double area() const = 0;
    virtual void draw() const = 0;
    
    virtual ~Shape() = default;
};
class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(double r) : radius(r) {}
    
    double area() const override {
        return 3.14159 * radius * radius;
    }
    
    void draw() const override {
        cout << "Drawing Circle" << endl;
    }
};
class Rectangle : public Shape {
private:
    double width, height;
    
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double area() const override {
        return width * height;
    }
    
    void draw() const override {
        cout << "Drawing Rectangle" << endl;
    }
};
int main() {
    // Shape shape;  // error: abstract class
    
    vector<unique_ptr<Shape>> shapes;
    shapes.push_back(make_unique<Circle>(5.0));
    shapes.push_back(make_unique<Rectangle>(4.0, 6.0));
    
    for (const auto& shape : shapes) {
        shape->draw();
        cout << "Area: " << shape->area() << endl;
    }
}

A few details in this example are worth calling out because they trip people up in real codebases. First, Shape declares virtual ~Shape() = default; — a virtual destructor is mandatory here because the code stores unique_ptr<Shape> and destroys derived objects (Circle, Rectangle) through the base pointer; without it, only ~Shape() would run and any derived-class members wouldn’t be properly cleaned up (see the “Virtual destructor” section below for why). Second, area() and draw() are both const — a design choice that says “computing area or drawing does not mutate the shape,” which lets you call these methods on const Shape& or const unique_ptr<Shape>& freely. If a derived override drops the const qualifier, it doesn’t actually override the base function at all — it silently declares an unrelated overload, and the base class’s pure virtual requirement remains unsatisfied, which is a compile error you want, not a silent runtime bug (this is one of the many reasons to always write override, covered in the pitfalls section). Third, vector<unique_ptr<Shape>> is the idiomatic way to hold a heterogeneous collection of polymorphic objects in modern C++: it avoids raw-pointer ownership bugs, avoids slicing (discussed further down), and still allows each stored Shape to be secretly a Circle or Rectangle underneath.

A filesystem tree, payment strategies, and loggers

These three examples move past the toy Animal/Shape cases into patterns you’ll actually reach for in application code. Each one uses virtual dispatch for a different reason, and recognizing which reason applies helps you decide whether virtual functions are the right tool versus, say, a std::variant with std::visit, or a template-based (CRTP) approach.

A file-system tree with the Composite pattern

A filesystem tree is a textbook case for the Composite pattern: File and Directory both need to answer print() and getSize(), but a Directory’s answer depends recursively on its children’s answers, which may themselves be files or further directories. Virtual dispatch is what lets Directory::getSize() call child->getSize() without caring whether child is a File or another Directory — the recursion terminates naturally because each concrete type knows how to compute its own contribution.

class FileSystemNode {
protected:
    string name;
    
public:
    FileSystemNode(const string& n) : name(n) {}
    virtual ~FileSystemNode() = default;
    
    virtual void print(int indent = 0) const = 0;
    virtual size_t getSize() const = 0;
};
class File : public FileSystemNode {
private:
    size_t size;
    
public:
    File(const string& n, size_t s) : FileSystemNode(n), size(s) {}
    
    void print(int indent = 0) const override {
        cout << string(indent, ' ') << "- " << name 
             << " (" << size << " bytes)" << endl;
    }
    
    size_t getSize() const override {
        return size;
    }
};
class Directory : public FileSystemNode {
private:
    vector<unique_ptr<FileSystemNode>> children;
    
public:
    Directory(const string& n) : FileSystemNode(n) {}
    
    void add(unique_ptr<FileSystemNode> node) {
        children.push_back(move(node));
    }
    
    void print(int indent = 0) const override {
        cout << string(indent, ' ') << "+ " << name << "/" << endl;
        for (const auto& child : children) {
            child->print(indent + 2);
        }
    }
    
    size_t getSize() const override {
        size_t total = 0;
        for (const auto& child : children) {
            total += child->getSize();
        }
        return total;
    }
};
int main() {
    auto root = make_unique<Directory>("root");
    
    auto docs = make_unique<Directory>("docs");
    docs->add(make_unique<File>("readme.txt", 1024));
    docs->add(make_unique<File>("guide.pdf", 5120));
    
    root->add(move(docs));
    root->add(make_unique<File>("main.cpp", 2048));
    
    root->print();
    cout << "Total size: " << root->getSize() << " bytes" << endl;
}

Notice that getSize() on Directory never checks “is this child a File or a Directory?” with an if/switch or a dynamic_cast — that kind of manual type-checking is exactly the code smell virtual functions exist to eliminate. Every time you see a chain of if (dynamic_cast<Foo*>(ptr)) / else if (dynamic_cast<Bar*>(ptr)), it’s usually a sign that the logic belongs in a virtual function on Foo and Bar instead. The recursive getSize() also demonstrates why virtual functions compose well: the parent doesn’t need special-case logic for “a directory containing directories” versus “a directory containing files” — the recursion is uniform because every FileSystemNode answers the same interface.

Swappable payment strategies

The Strategy pattern is probably the single most common practical use of virtual functions in application code: you want to select an algorithm or policy at runtime — how to pay, how to sort, how to compress — without hard-coding a big switch at every call site. ShoppingCart holds a unique_ptr<PaymentStrategy> and knows nothing about credit cards or PayPal; it just calls pay(). Swapping setPaymentStrategy() at runtime lets the same cart object change behavior mid-program, which would require a much more awkward design with function pointers or tagged unions.

class PaymentStrategy {
public:
    virtual ~PaymentStrategy() = default;
    virtual void pay(double amount) = 0;
};
class CreditCardPayment : public PaymentStrategy {
private:
    string cardNumber;
    
public:
    CreditCardPayment(const string& card) : cardNumber(card) {}
    
    void pay(double amount) override {
        cout << "Card " << cardNumber << " pays " 
             << amount << " KRW" << endl;
    }
};
class PayPalPayment : public PaymentStrategy {
private:
    string email;
    
public:
    PayPalPayment(const string& e) : email(e) {}
    
    void pay(double amount) override {
        cout << "PayPal " << email << " pays " 
             << amount << " KRW" << endl;
    }
};
class ShoppingCart {
private:
    unique_ptr<PaymentStrategy> paymentStrategy;
    double total = 0;
    
public:
    void setPaymentStrategy(unique_ptr<PaymentStrategy> strategy) {
        paymentStrategy = move(strategy);
    }
    
    void addItem(double price) {
        total += price;
    }
    
    void checkout() {
        if (paymentStrategy) {
            paymentStrategy->pay(total);
            total = 0;
        }
    }
};
int main() {
    ShoppingCart cart;
    cart.addItem(10000);
    cart.addItem(20000);
    
    cart.setPaymentStrategy(make_unique<CreditCardPayment>("1234-5678"));
    cart.checkout();
    
    cart.addItem(15000);
    cart.setPaymentStrategy(make_unique<PayPalPayment>("[email protected]"));
    cart.checkout();
}

One easy mistake to make with this pattern is forgetting the virtual destructor on PaymentStrategy — the example above declares virtual ~PaymentStrategy() = default; up front, which is correct, but it’s worth internalizing as a rule: any class that is meant to be used polymorphically through a base pointer, and that owns derived-class-specific resources, needs a virtual destructor from the moment it’s written, not retrofitted later once something starts leaking. unique_ptr<PaymentStrategy> will call delete on whatever concrete object it holds when the strategy is replaced or the cart is destroyed, and that delete goes through the base class’s destructor unless it’s virtual.

Console and file loggers

Logging abstractions are another everyday use of the Strategy idea: Application doesn’t know or care whether Logger writes to the console or to a file — it just calls log(). This is also a good illustration of dependency injection without a framework: setLogger() accepts any unique_ptr<Logger>, so tests can inject a mock logger (say, one that writes to an in-memory buffer) without touching Application’s code at all. That testability is a direct, practical payoff of designing around virtual interfaces rather than concrete types.

class Logger {
public:
    virtual ~Logger() = default;
    virtual void log(const string& message) = 0;
};
class ConsoleLogger : public Logger {
public:
    void log(const string& message) override {
        cout << "[Console] " << message << endl;
    }
};
class FileLogger : public Logger {
private:
    string filename;
    
public:
    FileLogger(const string& file) : filename(file) {}
    
    void log(const string& message) override {
        ofstream ofs(filename, ios::app);
        ofs << "[File] " << message << endl;
    }
};
class Application {
private:
    unique_ptr<Logger> logger;
    
public:
    void setLogger(unique_ptr<Logger> l) {
        logger = move(l);
    }
    
    void run() {
        if (logger) {
            logger->log("Application started");
            logger->log("Application finished");
        }
    }
};
int main() {
    Application app;
    
    app.setLogger(make_unique<ConsoleLogger>());
    app.run();
    
    app.setLogger(make_unique<FileLogger>("app.log"));
    app.run();
}

vtable and vptr

Every major compiler implements virtual dispatch the same way: one read-only table of function pointers per polymorphic class (the vtable), and one hidden pointer in every object (the vptr). A virtual call loads the vptr, reads the slot for that function, and jumps through it. Two consequences matter when using virtual functions. The call normally cannot be inlined unless the compiler can prove the dynamic type, which is what final helps with. And because the vptr is set by each constructor in turn, a virtual call inside a base-class constructor or destructor runs the base version, not the derived override. Object layout, the cost of the indirect call, multiple-inheritance thunks and devirtualization are covered in C++ VTable Explained; the language rules around binding, default arguments and construction are in Virtual Functions and vtables: How Dynamic Binding Works.

Virtual destructor

class Base {
public:
    virtual ~Base() {
        cout << "~Base()" << endl;
    }
};
class Derived : public Base {
private:
    int* data;
    
public:
    Derived() : data(new int[100]) {}
    
    ~Derived() {
        cout << "~Derived()" << endl;
        delete[] data;
    }
};
int main() {
    Base* ptr = new Derived();
    delete ptr;  // ~Derived() then ~Base()
}

The rule of thumb from Effective C++ (Scott Meyers) still holds today: any class intended to be used polymorphically — i.e., any class someone might delete through a pointer to it — must have either a public virtual destructor, or a protected non-virtual one that prevents deletion through the base at all. Conversely, if a class has no virtual functions and is never meant to be a polymorphic base (a small value type like a Point, say), you should not add a virtual destructor purely out of caution — it adds the vptr overhead to every instance and signals a design intent (“this is a base class for runtime polymorphism”) that isn’t true. final on a class is a good way to make that intent explicit when a class truly isn’t meant to be a base at all. Chaining constructors matters here too: destruction always runs from the most-derived class up to the base (~Derived() then ~Base()), mirroring construction’s base-to-derived order, so a virtual destructor is what guarantees that chain actually starts at the right place when you only have a base pointer in hand.

Non-virtual destructors, missing override, and slicing

Non-virtual destructor

// Bad
class Base {
public:
    ~Base() {
        cout << "~Base()" << endl;
    }
};
class Derived : public Base {
private:
    int* data;
    
public:
    Derived() : data(new int[100]) {}
    
    ~Derived() {
        cout << "~Derived()" << endl;
        delete[] data;  // not called through Base*!
    }
};
Base* ptr = new Derived();
delete ptr;  // leak
// Good: virtual destructor
class Base {
public:
    virtual ~Base() {
        cout << "~Base()" << endl;
    }
};

What actually happens in the “Bad” case above is instructive: delete ptr sees a static type of Base*, and because ~Base() is not virtual, the compiler generates a direct call to Base::~Base() — full stop. Derived::~Derived() is never invoked, so the delete[] data inside it never runs and the 100-int array leaked on the heap is gone for good; there’s no way to recover it after the fact. The standard actually classifies this as undefined behavior (not merely “guaranteed to leak”), meaning a compiler is technically permitted to do anything at all here, though in practice you almost always just get the leak described. Static analyzers (Clang-Tidy’s cppcoreguidelines-virtual-class-destructor, for instance) and -Wnon-virtual-dtor in GCC/Clang are specifically built to catch this class of bug at compile time — turning that warning on for any codebase with class hierarchies is close to free insurance.

Missing override

class Base {
public:
    virtual void func() {}
};
// Typo creates a new function
class Derived : public Base {
public:
    void fucn() {}  // typo
};
// With override: compile error
class Derived : public Base {
public:
    void fucn() override {}  // error
};

Without override, the first Derived above compiles perfectly cleanly — fucn() is simply a brand-new, unrelated member function that happens to live in a derived class. Nothing in the language connects it to Base::func(), so any code calling func() through a Base* silently keeps running Base::func() even though the author clearly intended to customize it. This is one of the nastiest categories of bug in C++: it produces no warning, no error, and often no crash — just quietly wrong behavior that surfaces much later, and usually far from the line that’s actually broken. Adding override (C++11) turns this into a hard compile error, because the compiler now checks that a function actually does override something with a matching signature in a base class — including matching const-ness, ref-qualifiers, and parameter types exactly. The rule of thumb: every override of a virtual function should carry the override keyword, with no exceptions. It costs nothing at runtime and it converts an entire category of silent bugs into build failures, which is strictly better for anyone maintaining the codebase after you.

Object slicing

Base b = d; copies only the Base part of d and gives b Base’s vptr, so b.func() calls Base::func, while Base* ptr = &d; ptr->func(); still reaches Derived::func. A by-value parameter such as void process(Shape s) slices every Circle passed to it. Pass polymorphic objects by reference or pointer and own them through std::unique_ptr; if you want value semantics for a hierarchy, that is a sign to use std::variant or a type-erasure wrapper instead. C++ Object Slicing covers the less obvious places it happens and how to make it a compile error.

Performance considerations and alternatives

Because a virtual call can’t be resolved until runtime, it blocks a large family of compiler optimizations that depend on knowing exactly which function runs at a given call site — most notably inlining. Modern compilers perform devirtualization when they can statically prove the dynamic type at a call site (for example, calling a virtual function on a local variable whose concrete type is visible, or on an object accessed through a final class or final method) — in those cases the indirect call collapses back into a direct, inlineable one, and the overhead disappears entirely. Marking a class or an individual override final doesn’t just document intent; it’s also a signal the compiler can use for devirtualization when it can prove no further override is possible.

For genuinely hot code paths — tight loops processing millions of small polymorphic objects per second, such as particle systems or SIMD-friendly numeric kernels — some C++ codebases substitute the Curiously Recurring Template Pattern (CRTP) for runtime virtual dispatch, trading dynamic flexibility for compile-time resolution and full inlining. That’s a real trade-off, though: CRTP loses the ability to store heterogeneous types behind one interface, form containers of mixed derived types, or link against implementations compiled separately. As with most performance work, the right default is to write the natural, virtual-function-based design first, and only reach for CRTP or other zero-overhead patterns once profiling shows the indirect calls are an actual bottleneck — not because virtual dispatch is “slow” in the abstract.

FAQ

When should you reach for virtual functions rather than some other mechanism? The strongest signal is that you have a fixed interface (a set of operations) and an open-ended, growing set of implementations that need to be selected or swapped at runtime — plugin systems, strategy objects, GUI widget hierarchies, and visitor-style tree traversal are the classic cases. If the set of “types” is small, fixed, and known at compile time, a std::variant with std::visit is often a better fit today; if you need compile-time-only polymorphism for maximum performance, CRTP or plain templates are the alternative.

Is the performance overhead of virtual calls something to worry about? In the overwhelming majority of application code, no — the extra indirection is on the order of nanoseconds and is invisible next to I/O, allocation, or even ordinary arithmetic. It becomes worth measuring only in call-heavy hot loops, and even then the first fix is usually final (to enable devirtualization) rather than a wholesale redesign away from virtual dispatch.

What exactly does = 0 do, and how is it different from an ordinary virtual function with an empty body? = 0 marks the function pure virtual, which makes the class itself abstract — the compiler refuses to instantiate it directly, and any derived class that doesn’t override every pure virtual function is abstract too. An ordinary virtual function with an empty (or any) body is still instantiable on its own; there’s no compiler-enforced contract that derived classes must override it.

Why does the override keyword matter if the code compiles fine without it? Without override, a typo’d or mismatched signature in a derived class silently creates an unrelated new function instead of overriding the base one — no warning, no error, just quietly wrong dispatch. override asks the compiler to verify the override relationship really exists, turning that entire bug class into a compile-time error.

When is a virtual destructor required? Any time an object might be destroyed (via delete, or via a smart pointer going out of scope) through a pointer or reference to a base class, and the derived class has anything that needs cleanup — heap allocations, file handles, locks. If a base class is never used polymorphically for ownership purposes, a virtual destructor is unnecessary overhead.

Where can you learn more? Effective C++ and Effective Modern C++ by Scott Meyers cover these idioms in depth with historical context; cppreference.com’s pages on virtual functions, abstract classes, and override/final are the canonical up-to-date reference for exact language rules; and the C++ Core Guidelines (sections C.35–C.139 in particular) codify most of the “always/never” rules mentioned throughout this article.