Search code examples
c++undefined-behaviortype-coercion

Function pointer coercion along a class heirarchy


Consider the code bellow:

#include <type_traits>
#include <iostream>

struct B {
    virtual const char* whoami() const { return "I am a B!\n"; };
};

struct D : B {
    const char* whoami() const override { return "I am a D!\n"; };
};

void foo_impl( B*(*pf)() )
{
    B* b = pf();
    std::cout << b->whoami();
}

template <class C>
auto foo( C*(*pf)() ) -> std::enable_if_t<std::is_base_of<B, C>::value> {
    foo_impl(reinterpret_cast<B*(*)()>(pf));
}

D *bar() {
    static D d_; // kludge to work around dangling pointer problem.
                 // Unrelated to the actual question
    return &d_;
}

int main() {
    foo(bar); // prints "I am a D!" on gcc 5.1
    return 0;
}

Function pointers of course cannot be coerced according to the standard, and maybe there is no need (we can always just return a B*), but please humor me. As far as I can tell, there is no possible violation of the LSP, so if it was possible, a function returning D* could be used in place of a function returning B* in a completely opaque manner.

The thin foo template preforms the static type checking I wish the compiler would do, and then throws the type information to the wind. At this point, I'm aware that unless I cast back to the original pointer type, the behavior is undefined (C++11 §5.2.10/6).

My question is therefore:

Is there a practical reason, unrelated to the standard, that can cause the above code to fail? Or is there another standard reference, which can assuage the unpleasantness of the UB in the above code?


Solution

  • The following statement of your post is not correct :

    a function returning D* could be used in place of a function returning B* in a completely opaque manner

    This is not true, because converting D* to B* is not just a cast : a shift in the address may be required. It does not in your example because you do not use multiple inheritance.

    Consider this example:

    #include <type_traits>
    #include <iostream>
    
    struct A {
      int i = 0;
      int whoami() const { return i; };
    };
    
    struct B {
      int j = 1;
      int whoami() const {
        std::cout << "Woohoo B is called.\n";
        return j;
      };
    };
    
    struct D : A, B {
    };
    
    void foo_impl(B *(*pf)()) {
      B *b = pf();
      char const * identity = (0 == b->whoami() ? "A" : "B");
      std::cout << "I am " << identity << std::endl;
    }
    
    template <class C>
    auto foo( C*(*pf)() ) -> std::enable_if_t<std::is_base_of<B, C>::value> {
          foo_impl(reinterpret_cast<B*(*)()>(pf));
    }
    
    D *bar() {
      static D d_;
      return &d_;
    }
    
    int main() {
      foo(bar);
      return 0;
    }
    

    This prints I am A, even though you thought you used a B. The function called is effectively B::whomai, but the data member underneath is the one from A, since the address has not been shifted as it would have been with a proper static_cast of the pointer.