Why and When to Use C++ virtual: Memory Layout, Polymorphism, and Performance
This article explains the purpose and proper use of C++'s virtual keyword, illustrating how virtual functions enable polymorphism, detailing class memory layout with vtables and vptrs, comparing performance impacts, and outlining when virtual functions and destructors are truly necessary.
Introduction
In many projects the virtual keyword is overused. This article explores the mechanism of virtual, its memory implications, and when it should actually be applied.
Why virtual is Needed
Consider a graphics library with Point2d and Point3d classes. Without virtual the following code prints only the 2‑D representation even when a 3‑D point is passed:
#include <stdio.h>
class Point2d {
public:
Point2d(int x = 0, int y = 0) : _x(x), _y(y) {}
void print() const { printf("Point2d(%d, %d)
", _x, _y); }
protected:
int _x;
int _y;
};
class Point3d : public Point2d {
public:
Point3d(int x = 0, int y = 0, int z = 0) : Point2d(x, y), _z(z) {}
void print() const { printf("Point3d(%d, %d, %d)
", _x, _y, _z); }
protected:
int _z;
};
int main() {
Point2d point2d;
Point3d point3d;
point2d.print(); // Point2d(0, 0)
point3d.print(); // Point3d(0, 0, 0)
return 0;
}When a generic print(const Point2d&) function is used, a Point3d instance still prints as a 2‑D point because the function calls the non‑virtual method of the static type. Declaring print as virtual in the base class fixes the problem.
class Point2d {
public:
virtual void print() const { printf("Point2d(%d, %d)
", _x, _y); }
};
int main() {
Point2d p2d;
Point3d p3d;
print(p2d); // Point2d(0, 0)
print(p3d); // Point3d(0, 0, 0)
return 0;
}Class Memory Layout (Non‑virtual)
In the C++ object model, non‑static data members are stored inside each object, while static members and member functions reside outside. Most compilers lay out members in declaration order. The memory layout of the non‑virtual Point2d on macOS (x86_64) is shown below:
Compilers also align members to their size and then align the whole class to the size of the largest member. Declaring members from largest to smallest reduces padding.
Derived Class Memory Layout (Non‑virtual)
The size of a derived class equals the size of its base class plus the size of its own members. The layout of non‑virtual Point3d is:
Memory Layout with virtual
Adding a virtual function introduces a virtual table (vtable) and a hidden pointer (vptr) placed at the beginning of each object. The following classes illustrate this:
class Point2d {
public:
Point2d(int x = 0, int y = 0) : _x(x), _y(y) {}
virtual void print() const { printf("Point2d(%d, %d)
", _x, _y); }
virtual int z() const { printf("Point2d get z: 0
"); return 0; }
virtual void z(int z) { printf("Point2d set z: %d
", z); }
protected:
int _x;
int _y;
};
class Point3d : public Point2d {
public:
Point3d(int x = 0, int y = 0, int z = 0) : Point2d(x, y), _z(z) {}
void print() const { printf("Point3d(%d, %d, %d)
", _x, _y, _z); }
int z() const { printf("Point3d get z: %d
", _z); return _z; }
void z(int z) { printf("Point3d set z: %d
", z); _z = z; }
protected:
int _z;
};Inspecting the vtable at runtime confirms that each virtual function is correctly dispatched.
int main() {
typedef void (*VF1)(Point2d*);
typedef void (*VF2)(Point2d*, int);
Point2d point2d(11, 22);
intptr_t *vtbl2d = (intptr_t*)*(intptr_t*)&point2d;
((VF1)vtbl2d[0])(&point2d); // Point2d(11, 22)
((VF1)vtbl2d[1])(&point2d); // Point2d get z: 0
((VF2)vtbl2d[2])(&point2d, 33); // Point2d set z: 33
Point3d point3d(44, 55, 66);
intptr_t *vtbl3d = (intptr_t*)*(intptr_t*)&point3d;
((VF1)vtbl3d[0])(&point3d); // Point3d(44, 55, 66)
((VF1)vtbl3d[1])(&point3d); // Point3d get z: 66
((VF2)vtbl3d[2])(&point3d, 77); // Point3d set z: 77
return 0;
}Virtual Destructors
Without a virtual destructor, deleting an object through a base‑class pointer calls only the base destructor. Adding virtual to the base destructor ensures the full chain is executed:
class Point { public: ~Point() { printf("~Point
"); } };
class Point2d : public Point { public: ~Point2d() { printf("~Point2d"); } };
class Point3d : public Point2d { public: ~Point3d() { printf("~Point3d"); } };
int main() {
Point *p1 = new Point();
Point *p2 = new Point2d();
Point2d *p3 = new Point2d();
Point2d *p4 = new Point3d();
Point3d *p5 = new Point3d();
delete p1; // ~Point
delete p2; // ~Point (no derived destructor)
delete p3; // ~Point2d~Point
delete p4; // ~Point2d~Point
delete p5; // ~Point3d~Point2d~Point
return 0;
}Making ~Point virtual changes the output to invoke the most‑derived destructor first.
// Only ~Point is declared virtual; rest unchanged
int main() {
// same allocations as above
delete p1; // ~Point
delete p2; // ~Point2d~Point
delete p3; // ~Point2d~Point
delete p4; // ~Point3d~Point2d~Point
delete p5; // ~Point3d~Point2d~Point
return 0;
}When to Use virtual
Many classes declare virtual unnecessarily, leading to extra pointer overhead and indirect call costs. In classes with few or no members this can double memory usage. Virtual calls also prevent certain compiler optimizations, adding measurable runtime overhead.
#include <stdio.h>
#include <time.h>
struct Point2d { int _x, _y; };
struct VPoint2d { virtual ~VPoint2d() {} int _x, _y; };
template <typename T>
T sum(const T &a, const T &b) {
T result;
result._x = a._x + b._x;
result._y = a._y + b._y;
return result;
}
template <typename T>
void test(int times) {
clock_t t1 = clock();
for (int i = 0; i < times; ++i) {
sum(T(), T());
}
clock_t t2 = clock();
printf("clocks: %lu
", t2 - t1);
}
int main() {
test<Point2d>(1000000);
test<VPoint2d>(1000000);
return 0;
}On the author's Mac, the non‑virtual version took ~12,819 clock ticks, while the virtual version took ~21,833 ticks – a 70% slowdown.
Therefore, only use virtual when:
Polymorphic behavior via base‑class pointers is required.
A base class needs a virtual destructor to ensure proper cleanup.
Otherwise, avoid virtual to save memory and improve performance. Even virtual base classes are discouraged unless absolutely necessary.
Conclusion
The article has covered the purpose, mechanics, memory layout, and performance impact of C++ virtual. Understanding these details helps you apply virtual judiciously, avoiding unnecessary overhead while leveraging true polymorphism when needed.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Alibaba Cloud Developer
Alibaba's official tech channel, featuring all of its technology innovations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
