Home > Net >  Whats the point of pointers to member functions?
Whats the point of pointers to member functions?

Time:05-29

When I get the address of a member function, I can't assign the address to a void*:

void* ptr = &object::function; // <-- doesn't work!

I understand that this is because member functions give you these weird pointer to members instead of just behaving like a normal function, but I don't understand why this is so.

Yes, the function has an extra argument for the this pointer. Yes, the function doesn't really make sense outside of the context of the relevant object. But in the end, the function is still at a specific memory address, just like every other function out there. Considering that, I don't understand why the language designers are making me jump through hoops just to get the memory address of a member function:

For normal function:

void function() { }
std::cout << (size_t)&function << '\n';

No funny business required for that, but for a member function:

struct object {
   void function() { }
}

void object::* ptr = &object::function;
std::cout << *(size_t*)&ptr << '\n';

It just gets weird for seemingly no reason. I sort of understand pointers to members in the context of member variables, where they don't actually have an address until you create an instance, so until then you use the pointer to member as a sort of pseudo pointer. But I don't understand pointers to member functions. They're still functions, why are they getting this weird special treatment, and what actual advantages do these pointers to member functions have?

CodePudding user response:

Note that according to the C and C specs, you can't cast any function pointer to a void*. It is a common extension to allow it for normal functions (and that extension is required by the POSIX standard, so is likely to remain common), but not so much for member function pointers.

The reason is that many implementations of member functions require additional fixups/lookup besides just calling the function implementation -- for a virtual function, you need to look up the implementation based on the dynamic type of this (usually a vtable lookup), and supporting mulitple inheritance may require some adjustment of this before calling the function. So many implementations use a representation for a method pointer that is larger than a void * and can't fit in it.

CodePudding user response:

I can't assign the address to a void*

but I don't understand why this is so.

The most direct explanation is that pointers to member function are not convertible to void*. The most direct explanation for that is that the language doesn't allow it. Pointers to members are not pointers (this applies to pointers to data members as well as pointers to member functions).

the function is still at a specific memory address

It's not so simple. There may actually be many implementations of the member function if it is virtual, and each implementation will be in a different memory address. The language implementation must somehow ensure that when you call through a pointer to member function on a derived object, the correct override is called. This becomes even more complex when multiple inheritance is involved. A pointer to member function typically contains more information than merely a single address.

If you check the sizeof the pointer to member function in your language implementation, then you'll likely find that it is bigger than the size of void*. Anything converted to void* must be convertible back to the same value, which is not possible when the original type has larger state than void* can represent.


Side note 1: Conversion from pointer to function to void* is a conditionally supported feature, so that may not be possible on all (old, esoteric?) language implementations either.

Side note 2:

std::cout << (size_t)&function << '\n';

Not only does this reliy on the aforementioned conditional feature, but this also relies on conversion from void* to std::size_t being well defined. That's not guaranteed either. When you want to convert a pointer to an integer, you should prefer to use std::uintptr_t instead.

Side note 3:

void object::* ptr = &object::function;

This is ill-formed. It looks like a pointer to data member of type void, which of course isn't allowed. I think you intended to write:

void (object::* ptr)() = &object::function;

Side note 4:

*(size_t*)&ptr

&ptr points to an object of type void (object::*)(), so reinterpreting it as size_t* and accessing the pointed object through the reinterpreted pointer results in undefined behaviour. Don't do this in a real program.

CodePudding user response:

But in the end, the function is still at a specific memory address, just like every other function out there.

False. (Well, it is true that each function has a specific address, but the idea that there is always a single "the function" for a particular value of a pointer-to-member-function makes this false.)

A pointer-to-member does not store a specific memory address. Rather, it stores something that can be combined with an object of a correct type to produce a specific memory address.

As an example, the following code

#include <iostream>

// Define some types to use in the example
struct Base {
   void function() { std::cout << "Function\n"; }
   virtual void virtualFunction() { std::cout << "Base\n"; }
};
struct Derived : Base {
   void virtualFunction() override { std::cout << "Derived\n"; }
};

int main()
{
    Base base;
    Derived derived;

    // Demonstrate pointing to a virtual function.
    void (Base::*pointer)() = &Base::virtualFunction;
    (base.*pointer)();
    (derived.*pointer)();

    // Demonstrate pointing to a non-virtual function.
    pointer = &Base::function;
    (base.*pointer)();
    (derived.*pointer)();
}

produces the following output

Base
Derived
Function
Function

The first two times that the variable pointer is invoked, two different functions are called, even though pointer did not change. This demonstrates that a pointer-to-member-function cannot simply hold the address of a single function. What it points to can depend on the object with which it is combined.

The second part of this example is to demonstrate that the same variable is equally capable of holding a pointer to a non-virtual function. The same type, even the same variable, needs to be able to handle both the case where dynamic dispatch is needed and the case where it is not. Simply storing the address of a non-virtual member function is insufficient since the value has to also contain the information "I do not point to a virtual function".

I tend to think of pointers-to-member as offsets. A pointer-to-data-member could be implemented as an offset into the object, and a pointer-to-member-function could be implemented as on offset into a virtual function table, where the vtable is viewed as being extended to also encompass the non-virtual member functions. If you have (the address of) an object of an acceptable type, then these offsets can be used (by the compiler) to get a specific address.

Keep in mind that viewing pointers-to-members as offsets is just one possibility. Others are possible (especially if sizeof(pointer) happens to be twice sizeof(std::ptrdiff_t)), so do not try to conclude that something must happen based upon this perspective.

  • Related