Why does a pure virtual member function have to be virtual?

184 Views Asked by At

I have a question regarding the declaration of pure virtual functions in C++. I come from a Java background, and so I see pure virtual functions as a way to define abstract classes and the idea of interfaces. My question is simple, when we define a pure virtual function in C++ we have to write something like this:

virtual void function() =0

The void could be of any type, but we have to include the equals to 0 and the virtual keyword. I understand the "equals to 0" part as being used to define a pure function, but my question is why does it have to be virtual? Couldn't we just define it without the virtual keyword? Is this just a part of C++ that is included in the definition of pure virtual functions or is there a logical reason as to why "abstract methods" must be virtual as well?

2

There are 2 best solutions below

0
Jan Schultke On

A non-virtual pure function doesn't make much sense. Fundamentally, a function is a named section of code that can be called in some way. If you make it pure, that means there is (possibly) no section of code at all; there is only the name.

This does make sense for virtual member functions because the implementation could be provided by one of the derived classes. Even if Base::foo() doesn't exist, it's possible that calling Base::foo() will call Derived::foo() through dynamic dispatch. However, without virtual, no such mechanism exists.

Similarly, abstract final methods don't make much sense in Java. A non-virtual member function in C++ can be considered final since there is no mechanism for overriding it.

0
LiuYuan On

Why does it have to be virtual? Couldn't we just define it without the virtual keyword?

The answer is, no. The mechanism of a virtual member function and an ordinary member function are quite different.

How a member function works

A member function's address is determined at the link period by the linker.

Check this code. In this piece of code, after linking, the void Test::member(void) part in call void Test::member(void) will be replaced by a real offset address, which means it's hard-coded. No matter whether there are derived classes, if you call member() on a reference/pointer of a Test, the function to be called is determined.

Yet it's not how virtual functions work in C++.

How a virtual member function works

In C++, if a class has virtual functions, it will have a virtual table. TLDR, here is a small example:

#include <iostream>

class Test {
  virtual void test() = 0;
};

int main() { 
  std::cout << "sizeof(Test): " << sizeof(Test) << std::endl; 
}
sizeof(Test): 8

Although Test is empty, it contains a pointer to the virtual table, which makes it possible for polymorphism.

By specifying a function virtual, only the compiler will put the address of the function's address into the virtual table, instead of replacing the call statement with a hard-coded function address.

Whenever you declare a virtual function, an item will be created in the vtable. In this example, once you declare Test::test, the first item will store the function address of Test::test, yet since it's a pure virtual function, the content of the item might be nullptr or whatever, depending on the implementation of compiler. Once you derive a class from Test, let's say Derived, and overridden test in Derived, the first item's content will be updated with the address of Derived::test.

When you get a pointer/reference of Test, and try to call test function of the object, the program will first get the first item in the vtable, get the address of <ObjectType>::test's address, then call the function at that address.

Here is a small example:

#include <iostream>

class Test {
public:
  virtual void test() = 0;
};

class Derived_A : public Test {
public:
  virtual void test() override { std::cout << "A::" << __func__ << std::endl; }
};

class Derived_B : public Test {
public:
  virtual void test() override { std::cout << "B::" << __func__ << std::endl; }
};

int main() {
  std::cout << "sizeof(Test): " << sizeof(Test) << std::endl;

  Derived_A a;
  Derived_B b;

  long *vtable = (long *)(&a);          // get vtable address
  long *first_item = (long *)(*vtable); // get the address of the first item
  void (*func)() = (void (*)())(*first_item);
}

result:

sizeof(Test): 8
A::test

Since we get Derived_A's vtable's address, the function we called with the first item of the table is Derived_A::test. Yet if you use the same method to operate on a Derived_B's vtable, you will call Derived_B::test, and that's how polymorphism works in C++.