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

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

이 글의 핵심

Practical guide to C++ virtual functions: runtime polymorphism, override safety, abstract interfaces, and patterns like strategy and logging.

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.

Functions whose dynamic type determines which override runs at runtime.

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;
}

virtual vs non-virtual

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;
}

Pure virtual functions

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;
    }
}

Practical examples

Example 1: File-system tree

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;
}

Example 2: Strategy pattern

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();
}

Example 3: Logger

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

class Base {
public:
    virtual void func1() {}
    virtual void func2() {}
};

// Conceptually:
// Base object = [vptr] + [members...]
// vptr -> vtable (addresses of func1, func2)

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()
}

Common pitfalls

Pitfall 1: 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;
    }
};

Pitfall 2: 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
};

Pitfall 3: Slicing

class Base {
public:
    virtual void func() {
        cout << "Base" << endl;
    }
};

class Derived : public Base {
public:
    void func() override {
        cout << "Derived" << endl;
    }
};

Derived d;
Base b = d;  // slices
b.func();    // Base

Base* ptr = &d;
ptr->func(); // Derived

FAQ

Q1: When to use virtual functions?

A:

  • Runtime polymorphism
  • Interface-style extension
  • Abstract base classes

Q2: Performance overhead?

A: Small—vtable indirection. Usually negligible vs flexibility.

Q3: Pure virtual functions?

A: = 0 makes the class abstract; derived types must implement.

Q4: override keyword?

A: C++11—documents intent and catches signature mistakes.

Q5: Virtual destructor required?

A: Whenever you delete derived objects through a base pointer.

Q6: Learning resources?

A:

  • Effective C++
  • cppreference.com
  • C++ Primer

  • Inheritance and polymorphism
  • Object slicing
  • VTable

Practical tips

Debugging

  • Warnings first

Performance

  • Profile

Code review

  • Conventions

Practical checklist

Before coding

  • Right approach?
  • Maintainable?
  • Performance?

While coding

  • Warnings?
  • Edge cases?
  • Errors?

At review

  • Intent?
  • Tests?
  • Docs?

Keywords

C++, virtual function, polymorphism, override, OOP


  • Inheritance
  • Virtual destructor
  • Object slicing
  • Classes and objects
  • CRTP pattern