1

Lets consider the following example

#include <iostream>

class Base {
public:
    virtual void foo() {
        std::cout << "Base::foo()" << std::endl;
    }
};

class Derived : public Base {
public:
    void foo() override {
        std::cout << "Derived::foo()" << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->foo();

    delete basePtr;
    return 0;
}

So when Derived class was allocated memory, a vptr was created inside Derived class that is pointing to the vtable inside the derived class. Now vtable contained the address of virtual function in the Derived class.

My question is basePtr itself is of type Base. How is it able to access the vptr which is present inside the Derived class. Because if basePtr tries accessing non virtual member function of derived class, we would have gotten compiler error. Then why not while accessing member variable vptr of Derived class?

H Kumar
  • 37
  • 6
  • 1
    The vtable is an implementation detail. In your example after optimizations have been applied no vtable or vptr is necessary. – 463035818_is_not_an_ai Jul 07 '23 at 06:19
  • 2
    A `vptr`, if it even exists, is not a member variable. – Ted Lyngmo Jul 07 '23 at 06:19
  • 1
    Frankly, your way of arguing in terms of accessing members and compiler errors makes no sense for the vtable. The implementation can do whatever it likes to implemented the behavior as specified. – 463035818_is_not_an_ai Jul 07 '23 at 06:21
  • 2
    Unrelated: `delete basePtr;` will not call the destructor of `Derived` since the base class destructor is not `virtual` - which is bad. – Ted Lyngmo Jul 07 '23 at 06:21
  • @463035818_is_not_an_ai so many blogs and tutorials states thatvptr is created inside all class that has virtual function and points to static vtable that contains address of functions – H Kumar Jul 07 '23 at 06:22
  • 2
    @HKumar None of those blogs claims (the potential) `vptr` to be a member variable - and if they actually do, delete your bookmarks to those blogs. – Ted Lyngmo Jul 07 '23 at 06:23
  • 1
    From chatgpt: `The vptr is usually implemented as a member variable of the object's memory layout, typically located at the beginning of the object's memory. The vptr points to the vtable of the corresponding class` I know it does not provide 100% correct results but what does it actually mean here – H Kumar Jul 07 '23 at 06:31
  • 1
    @HKumar Oh, whatever you do, do not trust ChatGPT with these questions. It's wrong. Here: [C++23 standard, latest draft](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/n4950.pdf) - Search in there for `vptr`/`vtable` and you will find _nothing_ - which would be strange if there was a `vptr` **member variable**. Instead, search for `virtual` and you'll find **tons** of information - none of which mentions a `vtable`. – Ted Lyngmo Jul 07 '23 at 06:33
  • Let's just take the _typical_ implementation. `Derived` is polymorphic, and so it contains virtual table. The table is a pointer, stored at the beginning of the memory allocated to `Derived`. Since the class does not itself have any members, its entire storage will consist of a pointer to the virtual table. That virtual table is a known address corresponding to the class `Derived`. An instance of `Base` will have a _different_ virtual table address. The compiler knows that `Base::foo` is virtual, and so it locates the _correct_ function pointer by invoking the object's virtual table. – paddy Jul 07 '23 at 06:36
  • Suggested duplicate: https://stackoverflow.com/questions/99297/how-are-virtual-functions-and-vtable-implemented – paddy Jul 07 '23 at 06:43
  • @TedLyngmo 1) Is there any C++ impl that doesn't use a vptr for such code, in general? (discounting the tiny special cases where all types are known at compile time) 2) When virtual calls are implemented via a ptr, how is the vptr not a special compiler generated, hidden, data member of the class with virtual functions? – curiousguy Aug 06 '23 at 06:40
  • @HKumar "_implemented as a member variable_" suggests that the impl generates C++ code, not asm nor binary, as these don't have a concept of member variable; you could write "as if by an invented data member", it would be more precise. I agree with ChatGPT with that small nuance. I don't understand the issue as **saying "implemented as a member" does not _in any way_ mean that the user code could name or use that member**. – curiousguy Aug 06 '23 at 06:45
  • @curiousguy 1. I don't know. 2. I objected to calling it a **member** variable, which it's clearly not: [class.mem.general](https://eel.is/c++draft/class.mem#general-1.sentence-1). If one called it a **special hidden** member variable, I wouldn't object as strongly, although I'd try to phrase it without calling it a _member_ at all since what a member of a class is, is defined in the standard and _"The member-specification in a class definition declares the full set of members of the class; **no member can be added elsewhere**."_ Perhaps _hidden associated variable_ would be better. – Ted Lyngmo Aug 06 '23 at 07:07
  • @TedLyngmo Even for `cfront`? – curiousguy Aug 06 '23 at 07:08
  • @curiousguy Sorry, I don't understand the question. – Ted Lyngmo Aug 06 '23 at 07:09
  • @TedLyngmo Would your previous comment "_objected to calling it a member variable_" also apply to cfront code? – curiousguy Aug 06 '23 at 19:54
  • @curiousguy To my knowledge, the c++ version that cfront parsed was never standardized so I couldn't really make the same argument there. – Ted Lyngmo Aug 06 '23 at 20:06

2 Answers2

1

My question is basePtr itself is of type Base. How is it able to access the vptr which is present inside the Derived class?

It's not, it's only accessing its own vptr, but that vptr is being modified by Derived during initialization. In C++ inheritance, the base class is contained in the derived class as a base class subobject. The layout is as follows:

class Base {
protected:
    vtable_t* vptr;
public:
    // ...
};

class Derived {
public:
    Base BaseSubobject;
    // ...
};

// probably passes, if a vtable pointer is as large as void*
static_assert(sizeof(Base) == sizeof(void*));

// will pass on any sane implementation
static_assert(sizeof(Derived) == sizeof(Base));

Notice that Derived doesn't have its own vptr, there only one vptr inside of the BaseSubobject. During When Derived gets initialized, it will first initialize the BaseSubobject inside, and then mutate the vptr inside of Base to become a pointer to the Derived vtable. The process looks something like this:

// Base constructor sets its vptr to the vtable of Base
Base::Base() : vptr(&Base::vtable) {}

// Derived constructor first initializes Base, then replaces the vptr inside
Derived::Derived() : BaseSubobject() {
    // replaces Base::vptr
    this->BaseSubobject.vptr = &Derived::vtable;
}

// dynamic dispatch process for virtual void foo():
void Base::__dispatch_to_foo__() {
    // step 1: fetch address of virtual function from Base::vptr,
    //         which may be a pointer to Derived::vtable
    void(*foo)(Base*) = this->vptr[__foo_index__];
    // step 2: call the fetched address
    foo(this);
}

On a side note, this is also why virtual calls won't dispatch to Derived in the Base constructor. While Base is being initialized, its vtable pointer hasn't been replaced by that of Derived yet.


Disclaimer 1: All uses of types, data members, and constructors are exposition-only. They're just meant to approximate what the compiler generates, and are not equivalent.

Disclaimer 2: vtables are an implementation detail. The C++ standard doesn't require polymorphism to be implemented this way, but most compilers use this technique.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
0

After researching on this topic a bit, here is the general overview that I understood. Please feel free to edit answer if you find any mistake.

vptr is typically stored as a hidden variable within each object of a class that has virtual functions. It is not stored directly within either the base or derived class itself, but within each object that is instantiated from the class.

  • Each object of a class with virtual functions will have its own vptr.
  • The vptr is added to the object's memory layout as a hidden member variable.
  • The vptr points to the vtable associated with the class, which contains the addresses of the virtual functions.
  • The vptr allows for dynamic dispatch of virtual functions at runtime based on the actual object's type.

During runtime, when virtual functions are called through a base class pointer or reference, the vptr is used to access the correct vtable and resolve the appropriate virtual function based on the actual object's type. i.e the type of object stored in pointer.

H Kumar
  • 37
  • 6
  • 2
    You must stop referring to it as a member variable. It is not a member, and it is not a variable. – paddy Jul 07 '23 at 06:42
  • 2
    you are putting the cart before the horse. C++ has dynamic dispatch and one way to implement it is using a vtable. It may seem like a nitpick, but read eg about devirtualization. When a virtual method is called then the right method is invoked, how this is achieved is secondary. – 463035818_is_not_an_ai Jul 07 '23 at 06:53
  • "how it happens" is what I want to understand, at least for one of the type of compiler. I searched other questions on SO but none provide internals of how?? – H Kumar Jul 07 '23 at 06:55
  • @HKumar: How it happens can differ from class to class, even from call to call, even for a single compiler. All you can say is that each object of polymorphic type must contain *some* information about its type, in case the compiler has no other source of type information available. Using a pointer makes sense, since the type information can be shared. But at this level, you can't even talk about the difference between pointers and references. – MSalters Jul 07 '23 at 09:06
  • @HKumar - The answer is ok as a general description of how it *can* be implemented. But note that in your specific example, clang will see that we only ever have a `Derived` object, so it optimizes out *everything*, including `new`, `delete`, the pointer, and the function call. It just inlines `std::cout << "Derived::foo()"` into main, and that's it! – BoP Jul 07 '23 at 09:54
  • @463035818_is_not_an_ai "_one way to implement it is using a vtable._" I keep asking if any one has ever seen any C++ impl (commercial, research purpose, serious, toy, or anything) attempting to use anything else for the implementation of dynamic dispatch, in the real world. That is, not necessarily a serious C++ impl, but at least a real tool, not a concept written on a napkin. Until such compilers are shown to exist, people could write "the common impl strategy of known compilers" instead of "some random choice by some compilers" (lol). – curiousguy Aug 06 '23 at 06:49
  • "_vtable associated with the class, which contains the addresses of the virtual functions._" it does that and some more: the vtable identifies the unique type; it lets you find the name of the class as a string; it allows equality comparison of class type; it handles safe downcast (`dynamic_cast`), and it lets you find the address of the complete object (`dynamic_cast`) and it's often used to find where the virtual base subjects reside. That's why the so called "virtual function pointers table" contains a bunch of small integers (offsets) and not just pointers. – curiousguy Aug 06 '23 at 06:54
  • @MSalters "_How it happens can differ from class to class, even from call to call, even for a single compiler._" The size and layout of even a simple C struct can differ "even for a single compiler" according to the "memory model" and compiling options governing the size of fundamental types. But using the same "memory model" and variable size options will generate compatible code, hopefully. Similarly, for C++ code, using the same target arch/codegen options generates code using common conventions and won't change at random. – curiousguy Aug 06 '23 at 06:58
  • @BoP Special cases can be optimized, but there is always a general case that cannot. To avoid hitting special cases, it's always better to use harder to optimize cases, like using globally visible variables, function parameters, separately compiled functions or (if you aren't allergic), dare I write, `volatile` (you can get _censored_ on SO for mentioning such extreme keyword). – curiousguy Aug 06 '23 at 07:07