Size

An empty class (with no data members) has a size of 1, because references to two such objects will not be different if size were 0. However an empty class with a virtual method has a size of 8 rather than being 1.

Consider the following snippet:

class Nothing {
};
 
class NothingWithMethod {
public:
    void print() {}
};
 
class NothingWithVirtualMethod {
public:
    virtual void print() {
 
    }
};
 
int main() {
    Nothing nothing;
    NothingWithMethod nothingWithMethod;
    NothingWithVirtualMethod nothingWithVirtualMethod;
    std::cout << sizeof(nothing) << "\n";                  // 1
    std::cout << sizeof(nothingWithMethod) << "\n";        // 1
    std::cout << sizeof(nothingWithVirtualMethod) << "\n"; // 8
}

Now that we have verified some data is there in the empty class with a virtual method, it is time to find out that these values are.

Dynamic Dispatch

A class with virtual method contains a vptr (virtual pointer) pointing to a vtable (virtual table). This mechanism enables dynamic dispatch.

class Base {
public:
    virtual void print() {
        std::cout << "Base print\n";
    }
};
 
class Derived : public Base {
public:
    void print() override {
        std::cout << "Derived print\n";
    }
};
 
int main() {
    Base base;
    Derived derived;
 
    size_t ptr_size = sizeof(void*);
 
    void* base_vptr = nullptr;
    void* derived_vptr = nullptr;
 
    std::memcpy(&base_vptr, &base, ptr_size);
    std::memcpy(&derived_vptr, &derived, ptr_size);
 
    std::cout << "Base object vptr:    " << base_vptr << "\n";
    std::cout << "Derived object vptr: " << derived_vptr << "\n";
 
    // Call virtual method via base pointer
    Base* ptr = &derived;
    std::cout << "Calling print via Base* ptr to Derived:\n";
    ptr->print();  // Should call Derived::print()
 
    return 0;
}

This is the foundation of dynamic dispatch.

First define another virtual method so that we have more than one entry in the virtual table:

class Base {
public:
    virtual ~Base() = default;
    virtual void parse() {
        std::cout << "Base parse\n";
    }
};
 
class Json : public Base {
public:
    void parse() override {
        std::cout << "Json parse\n";
    }
};
 
class Csv : public Base {
public:
    void parse() override {
        std::cout << "Csv parse\n";
    }
}

We will require a factory function that creates the right parser based on file extension

std::unique_ptr<Base> createParser(const std::string& filename) {
    auto pos = filename.rfind('.');
    if (pos == std::string::npos) {
        return std::make_unique<Base>();  // No extension, default parser
    }
 
    std::string ext = filename.substr(pos + 1);
    if (ext == "json") {
        return std::make_unique<Json>();
    } else if (ext == "csv") {
        return std::make_unique<Csv>();
    } else {
        return std::make_unique<Base>();
    }
}

We can see dynamic dispatch in action like so

std::string files[] = { "data.json", "records.csv", "unknown.txt" };
for (const auto& filename : files) {
    std::cout << "Parsing file: " << filename << "\n";
 
    // Create the right parser dynamically based on file extension
    std::unique_ptr<Base> parser = createParser(filename);
 
    // Call the virtual method
    parser->parse();
 
    std::cout << "\n";
}

Vptr and Vtable

We can go a step further and call the Csv::parse() or Json::parse() method, not via references but rather raw function pointers which we will get from the virtual table explicitly.

// Define function pointer type: a function taking Base* and returning void
using ParseFuncPtr = void(*)(Base*);
 
void indirect_call(Base* obj) {
 
    void* vptr = nullptr;
    std::memcpy(&vptr, obj, sizeof(void*));
    std::cout << "vptr address: " << vptr << "\n";
 
    // Cast vptr to a pointer to array of function pointers (vtable)
    void** vtable = reinterpret_cast<void**>(vptr);
 
    // Let's assume 'parse' is the first entry in the vtable
    ParseFuncPtr fn = reinterpret_cast<ParseFuncPtr>(vtable[0]);
 
    std::cout << "Calling via vtable manually: ";
    fn(obj);  // manual indirection via vtable
}

Call this in the main function

Json jsonObj;
Csv csvObj;
 
indirect_call(&jsonObj);
indirect_call(&csvObj);