2

So a task: we have a third party library, there is a class (call it Base). There is a hidden implementation provided by the library call it Impl. I need to write a Proxy. Unfortunately Base has a protected virtual function fn.

So the question is how much the code below is correct from C++ viewpoint? It currently works perfectly in Visual Studio and doesn't work in clang/gcc on Mac (but compiles without any warnings). I quite realize mechanisms which happen there, so if remove class Problem everything works on both platforms. I'd want to know if I should report a bug to clang or it's undefined/unspecified behavior of C++ standard.

Expected result of the code is to call Impl::fn() normally

class Base
{
protected:
    virtual void fn(){}
};

class Impl : public Base
{
public:
    Impl() : mZ(54){}
protected:

    virtual void fn()
    {
        int a = 10; ++a;
    }

    int mZ;
};

class Problem
{
public:
    virtual ~Problem(){}
    int mA;
};

class Proxy :  public Problem, public Base
{
public:
    virtual void fn()
    {
        Base * impl = new Impl;

        typedef void (Base::*fn_t)();
        fn_t f = static_cast<fn_t>(&Proxy::fn);
        (impl->*f)();

        delete impl;
    }
};

int main()
{
    Proxy p;
    p.fn();
}
dev_null
  • 1,907
  • 1
  • 16
  • 25

2 Answers2

1

It crashes on precisely this line:

    (impl->*f)();

Trying to access memory behind an allocated block. This is usually a hint that one did not setup this correctly, and indeed, swapping the inheritance order fixes the issue, confirming this theory.

    Base * impl = new Impl;

    typedef void (Base::*fn_t)();
    fn_t f = static_cast<fn_t>(&Proxy::fn);
    (impl->*f)();

So the issue is actually where fn_t points to (certainly not the vtable entry of Base::fn here).

Now we see the issue truly. You try to call a protected function of another object, trying to use &Base::fn for this is not possible, trying to use a pointer to Proxy::fn is effectively a different function, with a different vtable index, which doesn't exist in Base.

Now this works just because MSVC uses a different memory layout, where coincidentally Proxy::fn and Base::fn have the same vtable index. Try swapping the inheritance order in an MSVC build and it might crash. Or try adding another function or member somewhere, sooner or later it will crash with MSVC, too, I guess.

About the basic idea: what we try to accomplish here is to call a protected function of a different object. Referring to this list, essentially the same is said here

Class members declared as protected can be used only by the following:

  1. Member functions of the class that originally declared these members.
  2. Friends of the class that originally declared these members.
  3. Classes derived with public or protected access from the class that originally declared these members.
  4. Direct privately derived classes that also have private access to protected members.
  1. not the case
  2. no friends declared
  3. trying to call method on different object, not this
  4. not the case

So I don't think this is legal, resulting in undefined behaviour, indifferent of any clever casting etc.

dom0
  • 7,356
  • 3
  • 28
  • 50
  • Thanks, I'm aware about how it works and why it crashes. And it won't crash with MSVC. What I want to find out is if this is a correct (a bit weird though) c++ code which has to work everywhere. BTW I'm glad you realize where are subtleness and how it's supposed to work :) – dev_null Oct 08 '14 at 12:32
  • I don't think this is well-defined behaviour, since you're effectively calling a protected function of a *different* object, which is not a legal thing to do. – dom0 Oct 08 '14 at 12:49
  • @dev_null what do you mean correct ? You are purposefully circonventing the standard to do something that is specified as illegal. Of course it does not "have to work everywhere" that is the point of the standard : to define what has to work. tl;dr : No. – Félix Cantournet Oct 08 '14 at 13:24
  • In regard to visual studio after your next edit: No it works independently on order of classes and number of virtual functions. IMHO it's right. VS uses another model to represent pmfs (pointers to memeber functions). In regard to calling a protected function: I'm able to call it on another object if the object is of the same type. that is calling Proxy::fn() of mMyOtherProxyObject is completely ok. What is essential here is casting of pointers (from Proxy to Base and from Base to Impl). I can even change question: what if everything here is 'public'. Should the example work ? – dev_null Oct 08 '14 at 13:44
  • Well, if everything was indeed public you would be able to call the function directly trough the base pointer... – dom0 Oct 08 '14 at 13:50
1

The issue is that you are multiply inheriting from both Base and Problem. The ABI layout of classes is not defined by the standard, and implementations can choose how they layout objects, which is why you see different results on different compilers.

Specifically, the reason for the crash is that your derived class ends up with two v-tables: one each for Base and Problem.

I the g++ case, since you inherit public Problem, public Base the class layout has the v-table for Problem in the "traditional" location, and the v-table for Base later in the class layout.

If you want to see this in action, add this to your main...

int main()
{
    Proxy p;
    Base *base = &p;
    Problem *problem = &p;
    std::cout << "Proxy: " << &p << ", Problem: " << problem << ", Base: " << base << '\n';
}

You will see something similar to this...

Proxy: 0x7fff5993e9b0, Problem: 0x7fff5993e9b0, Base: 0x7fff5993e9c0

Now, you are doing something "evil" here:

typedef void (Base::*fn_t)();
fn_t f = static_cast<fn_t>(&Proxy::fn);
(impl->*f)();

because you are taking a member function pointer for Proxy and applying it to an Impl object. Yes, they both inherit from Base, but you have given it a member function pointer for class Proxy and when it looks up that v-table, they are in different places.

You really just want to get the member function pointer for Base but since you are doing it from within the context of Proxy you can only access the Proxy member function. It should be obvious now that this is not desirable because of the multiple inheritance.

However, you can easily enough get what I think you want with a little helper class...

virtual void fn()
{
    typedef void (Base::*fn_t)();
    struct Helper : Base {
      static fn_t get_fn() { return &Helper::fn; }
    };

    Base * impl = new Impl;
    fn_t f = Helper::get_fn();
    (impl->*f)();
    delete impl;
}

Because Helper inherits from Base it has access to the protected member, and you can access it outside the multiple inheritance context of Proxy.

Jody Hagins
  • 27,943
  • 6
  • 58
  • 87
  • Thanks, since Proxy is on my side I'm able to chose order of inheritance. as well as I realize problem with multiple inheritance. again the main question is in which degree the code is correct (independently on compiler and order of inheritance). That is have I right to expect that this will work on Intel Compiler for example, or on C++ compiler 10 years later on another CPU/platform. – dev_null Oct 08 '14 at 14:03
  • You can make no assumptions about it at all, except that it will probably break with different compilers if you do what you are doing. The helper workaround should provide what you want with any compiler. – Jody Hagins Oct 08 '14 at 15:31