Skip to content

Last updated:2026-10-12

C++ Polymorphism: Virtual Functions, CRTP, and variant

C++ Polymorphism Performance Test: CRTP vs std::variant vs virtual

Polymorphism is an important concept in object-oriented programming: it lets a single interface stand for different types. C++ offers several ways to implement polymorphism; this article discusses three polymorphism scenarios:

  1. Virtual functions: the traditional way to implement polymorphism in C++ is to use virtual functions. This involves using a base class and derived classes to provide specific implementations.
  2. std::variant: introduced in C++17, std::variant implements a kind of polymorphism without inheritance.
  3. CRTP (Curiously Recurring Template Pattern): CRTP is a rather special technique that achieves polymorphism through the curious recurring pattern of templates.

The combinations tested are as follows:

  • Plain CRTP
  • CRTP + std::variant
  • virtual
  • std::variant + std::visit
  • std::variant + std::get_if
  • std::variant + std::holds_alternative

Compilers used:

  • gcc 13.2
  • clang17.0

The complete test code is in the Knowledge Planet; the key code is pasted here

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

class Circle : public Shape
class Rectangle : public Shape



// virtual
static void BM_VirtualFunction(benchmark::State& state) {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.reserve(1000);

    for (int i = 0; i < 1000; ++i) {
        if (i % 2 == 0) {
            shapes.emplace_back(std::make_unique<Circle>(5.0));
        } else {
            shapes.emplace_back(std::make_unique<Rectangle>(4.0, 6.0));
        }
    }

    for (auto _ : state) {
        double totalArea = 0.0;
        for (const auto& shape : shapes) {
            totalArea += shape->area();
        }
        benchmark::DoNotOptimize(totalArea);
    }
}
BENCHMARK(BM_VirtualFunction);

static void BM_VariantVisit(benchmark::State& state) {
    std::vector<std::variant<Circle, Rectangle>> shapes;
    shapes.reserve(1000);

    for (int i = 0; i < 1000; ++i) {
        if (i % 2 == 0) {
            shapes.emplace_back(Circle(5.0));
        } else {
            shapes.emplace_back(Rectangle(4.0, 6.0));
        }
    }

    for (auto _ : state) {
        double totalArea = 0.0;
        for (const auto& s : shapes) {
            totalArea += std::visit([](const auto& shape) { return shape.area(); }, s);
        }
        benchmark::DoNotOptimize(totalArea);
    }
}
static void BM_CRTPVariant(benchmark::State& state) {
    std::vector<ShapeVariant> shapes;
    shapes.reserve(1000);

    for (int i = 0; i < 1000; ++i) {
        if (i % 2 == 0) {
            shapes.emplace_back(crtp_light::Circle(5.0));
        } else {
            shapes.emplace_back(crtp_light::Rectangle(4.0, 6.0));
        }
    }

    for (auto _ : state) {
        double totalArea = 0.0;
        for (const auto& s : shapes) {
            totalArea += std::visit([](const auto& shape) { return shape.area(); }, s);
        }
        benchmark::DoNotOptimize(totalArea);
    }
}
BENCHMARK(BM_CRTPVariant);

// ...

Result 1: compiled with gcc. virtual and std::variant perform about the same, but both differ greatly from CRTP.

gcc bm

Result 2: compiled with clang. The overall trend is similar to the gcc build, except that CRTP + std::variant clearly regresses, which may also be due to the use of std::visit here.

clang bm

The book A Tour of C++ puts it like this:

This is basically equivalent to a virtual function call, but potentially faster. As with all claims of performance, this ''potentially faster'' should be verified by measurements when performance is critical. For most uses, the difference in performance is insignificant.

This broadly matches the present measurements.

Comments