Fundamentals 15 min read

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.

Alibaba Cloud Developer
Alibaba Cloud Developer
Alibaba Cloud Developer
Why and When to Use C++ virtual: Memory Layout, Polymorphism, and Performance

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performanceCmemory layoutPolymorphismvirtual functionsdestructors
Alibaba Cloud Developer
Written by

Alibaba Cloud Developer

Alibaba's official tech channel, featuring all of its technology innovations.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.