Virtual functions in C++ enable dynamic dispatch: when you call a member function through a base pointer or reference, the version that runs is chosen at run time from the actual type of the object, not the static type of the expression. That single mechanism turns a family of related classes into something you can handle generically, and it is the foundation of every plugin system, GUI framework and strategy pattern in C++.
Static binding versus dynamic binding
Without virtual, the call is resolved at compile time from the type of the pointer or reference.
#include
#include
class Shape {
public:
explicit Shape(const std::string& name) : name_(name) {}
double area() const { return 0.0; }
const std::string& name() const { return name_; }
private:
std::string name_;
};
class Circle : public Shape {
public:
Circle(const std::string& name, double radius)
: Shape(name), radius_(radius) {}
double area() const { return 3.14159 * radius_ * radius_; }
private:
double radius_;
};
int main() {
Circle disc("disc", 2.0);
Shape& as_shape = disc;
std::cout << "through base: " << as_shape.area() << '\n';
std::cout << "direct call: " << disc.area() << '\n';
return 0;
}The output shows the problem: the base version ran even though the object is a circle.
through base: 0
direct call: 12.5664That is static binding. The compiler saw a Shape reference and called Shape::area.
Adding virtual
Marking the base function virtual changes the rule: the compiler builds a table of the virtual functions for each class and looks up the right one at run time.
#include
#include
class Shape {
public:
explicit Shape(const std::string& name) : name_(name) {}
virtual double area() const { return 0.0; }
const std::string& name() const { return name_; }
private:
std::string name_;
};
class Circle : public Shape {
public:
Circle(const std::string& name, double radius)
: Shape(name), radius_(radius) {}
double area() const override { return 3.14159 * radius_ * radius_; }
private:
double radius_;
};
int main() {
Circle disc("disc", 2.0);
Shape& as_shape = disc;
std::cout << "through base: " << as_shape.area() << '\n';
std::cout << "direct call: " << disc.area() << '\n';
return 0;
}Now the derived version runs in both cases:
through base: 12.5664
direct call: 12.5664The override keyword is not required by the language, but it is worth always writing. If the signature does not match the base exactly, the compiler rejects the program instead of silently creating a second, unrelated function.
How the dispatch works
The compiler adds a hidden pointer to every class that has virtual functions. That pointer points at a vtable, an array of function pointers, one slot per virtual function in the class.
Calling a virtual function through a base pointer therefore becomes: read the object's vtable pointer, read the entry for that function, and call it. The cost is one extra indirection, which is why virtual calls are slightly slower than direct ones and why the compiler can devirtualise a call when it can prove the dynamic type.
#include
#include
#include
#include
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
virtual const char* kind() const = 0;
};
class Rectangle : public Shape {
public:
Rectangle(double width, double height) : width_(width), height_(height) {}
double area() const override { return width_ * height_; }
const char* kind() const override { return "rectangle"; }
private:
double width_;
double height_;
};
class Circle : public Shape {
public:
explicit Circle(double radius) : radius_(radius) {}
double area() const override { return 3.14159 * radius_ * radius_; }
const char* kind() const override { return "circle"; }
private:
double radius_;
};
int main() {
std::vector> shapes;
shapes.push_back(std::make_unique(3.0, 4.0));
shapes.push_back(std::make_unique<circle>(2.0));
double total = 0.0;
for (const auto& shape : shapes) {
std::cout << shape->kind() << ": " << shape->area() << '\n';
total += shape->area();
}
std::cout << "total: " << total << '\n';
return 0;
}</circle>The output shows a single loop handling two different types:
rectangle: 12
circle: 12.5664
total: 24.5664This is the payoff of dynamic dispatch: the loop knows nothing about rectangles or circles. It only knows the interface.
The rules of overriding
- A virtual function is overridden by a derived function with the same name, the same parameter types and the same return type. A different parameter list is an overload, not an override.
- The access level of the base does not matter. A public base function can be overridden by a private derived one.
- The override can be
constonly if the base isconst. - A
staticmember function cannot be virtual. - A member function that calls another virtual function dispatches on the *dynamic* type of the object, so the call inside the base reaches the derived override.
#include
class Shape {
public:
virtual ~Shape() = default;
double total_area() const {
return area() * 2.0; // dispatches to the derived area()
}
virtual double area() const { return 0.0; }
};
class Square : public Shape {
public:
explicit Square(double side) : side_(side) {}
double area() const override { return side_ * side_; }
private:
double side_;
};
int main() {
Square square(3.0);
std::cout << "area: " << square.area() << '\n';
std::cout << "total: " << square.total_area() << '\n';
return 0;
}The output shows the base's helper reaching the derived implementation:
area: 9
total: 18Pure virtual functions and abstract classes
A virtual function can be declared with = 0, which makes it pure virtual. A class with a pure virtual function cannot be instantiated; it is abstract and exists only as a base.
#include
#include
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
virtual std::string describe() const {
return "shape with area " + std::to_string(area());
}
};
class Triangle : public Shape {
public:
Triangle(double base, double height) : base_(base), height_(height) {}
double area() const override { return base_ * height_ / 2.0; }
private:
double base_;
double height_;
};
int main() {
Triangle triangle(6.0, 4.0);
std::cout << triangle.describe() << '\n';
return 0;
}The output shows the base's non-virtual helper using the derived implementation of the pure virtual one:
shape with area 12.000000Quick reference
- Without
virtual, the call is resolved from the static type at compile time. - With
virtual, the call is resolved from the dynamic type at run time. - An override needs the same name, parameter types and return type;
overridemakes the compiler check it. - The vtable is an array of function pointers per class, reached through a hidden pointer in every object.
- A pure virtual function (
= 0) makes the class abstract. virtual ~Shape() = default;in the base is what makes polymorphic deletion safe.
Try it yourself: write a [[Shape]] base with a non-virtual [[area]] method and two derived classes. Call [[area]] through base references and note that the base version runs. Then add [[virtual]] and [[override]], and call the same way. Finally, store the shapes in a [[std::vector]] of base pointers and sum their areas with a single loop.