c++function-pointersreinterpret-cast

Call function through pointer with no arguments


I want to implement a generic function returning a default value, and then use it through a function pointer as a replacement for other functions with different prototypes.

For example:

int replacement() {
  return 43;
}

int (*abstract)(int i, const char *c);

int main() {
    abstract = reinterpret_cast<decltype(abstract)>(replacement);
    int res = (*abstract)(55, "Test");

    std::cout << "Result is " << res << std::endl;
}

This code works, and actually does what I expect, but according to the standard it's undefined behaviour. Or is it?

Can I rely on the ABI to do the right thing? The parameters are passed in registers or pushed onto the stack, then if they are not used, it shouldn't be a problem. The reinterpret_cast or other equivalent cast is used for example in GTK+ and in Vulkan to pass a generic function type as pointer and letting the developers un-cast the right function type on the other end.

Is it just working by chance, or can I rely on this behaviour?


Solution

  • It's working purely by chance. You can never rely on undefined behavior.

    1. It may work for your particular CPU/OS combo due to the ABI being used. But the code is completely unportable. For example, in old 32-bit Win32 stdcall convention, it would corrupt the stack.
    2. The compiler may always optimize the code under the assumption that UB doesn't happen. It proves that the function pointed to doesn't fit the call signature? Then the call must be dead code, or perhaps the assignment must be really dead code. Consider:

    .

    int stub() { return 42; }
    int real(int i) { return i * 2; }
    
    void foo(bool active) {
     int (*ptr)(int);
      if (active) {
        ptr = &real;
      } else {
        ptr = reinterpret_cast<int (*)(int)>(&stub);
      }
      int arg = ptr(99);
      if (active) {
        submit_request(arg);
      }
    }
    

    The compiler could see that active == false leads to UB and therefore optimize foo under the assumption that active == true always.

    (No compiler I've tried does that, but that only means they haven't bothered to add such an optimization yet.)

    Here's what you should do instead: variadic templates let you define a stub for any signature you need:

    #include <cstdio>
    
    void submit_request(int);
    
    template <typename... Args>
    int stub(Args...) {
        std::printf("stub");
        return 42;
    }
    
    int real(int i) { return i * 2; }
    
    void foo(bool active) {
        int (*ptr)(int);
        if (active) {
            ptr = &real;
        } else {
            ptr = static_cast<int (*)(int)>(&stub);
        }
        int arg = ptr(99);
        if (active) {
            submit_request(arg);
        }
    }