Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

calling inline member in derived class from base class of derived instance

I've got two classes, let's call them A and B, that look something like this:

class A {
public:
    inline void f1(int n) { f2(n&1); }
    inline void f2(int n) { } // no-op in base class
};

class B : public A {
public:
    inline void f2(int n) { printf("n=%d\n",n); } // printf for debugging only
};

Now I'm instantiating B, then calling f1.

int main() {
    B myB;

    myB.f1(500);
}

What I expect to happen, is that it will call f1() in B, which is inherited from A, which will in turn take the LSB of 500 and pass it to f2() in B, which will print it out. Instead what happens is that it calls f1() in A.

Why isn't this working? The compiler knows at compile time that myB is of class B, not A. I can't make either f1 or f2 be virtual for two reasons: first, because for performance reasons I can't tolerate any function call overhead, and second, making it inline with some constant parameters allows the compiler to often optimize out a lot of the code. I have tried making f1() be inline virtual in A, but even with -O3 I am seeing (in one particular test with slightly more code which is irrelevant to this example) about about 500 bytes of assembled code versus 8 bytes if I simply do the equivalent of the following:

class A {
public:
    inline void f1(int n) { f2(n&1); }
    inline void f2(int n) { } // no-op in base class
};

class B : public A {
public:
    inline void f1(int n) { f2(n&1); }
    inline void f2(int n) { printf("n=%d\n",n); } // printf for debugging only
};

Now obviously I could just copy/paste f1 from A to all derived classes, but in the actual code this will wind up being about 10 classes and around 35 functions each instead of 1, and it seems rather hokey to keep #includeing the same code over and over to avoid the incredible maintenance issues with replicating this code in lots of places.

like image 741
Michael Avatar asked Mar 02 '26 10:03

Michael


2 Answers

There are two problems here:

  1. calling f2() in the context of A actually calls A::f2() - period. If you want polymorphic behaviour you must make f2 virtual.

  2. You have assumed that making f2 virtual will add runtime overhead. In actual fact, if the compiler can prove that you are calling f1() on a B, there will be no overhead whatsoever.

Proof here: https://godbolt.org/g/2OWPo2

As a bonus, the inline keywords are unnecessary. If you define the body of a method at the point of declaration, that method is implicitly inline.

In addition (and counterintuitively), inline does not cause generation of inlined code. It simply warns the compiler that it might see the definition more than once.

Inlining is an optimisation that the compiler will do on its own.

Finally, look again at the generated assembly. You will see that the compiler didn't even bother to emit a vtable or any vtable construction. Optimisers these days are really good.

like image 168
Richard Hodges Avatar answered Mar 05 '26 00:03

Richard Hodges


It's actually very common that you can't afford a virtual function. In that case you need a bit of redesign:

template<typename Impl>
class ABase {
public:
    inline void f1(int n) { f2(n&1); }
    inline void f2(int n) { static_cast<Impl*>(this)->f2(n); }
};

class AReal : public ABase<AReal> {
    inline void f2(int n) { } // no-op here
};

class B : public ABase<B> {
public:
    inline void f2(int n) { printf("n=%d\n",n); } // printf for debugging only
};

Note that you still can't expect dynamic polymorphism and that AReal and B are now unrelated. (If the latter is not desired, it can be fixed by adding a non-template base to ABase; the former can't be fixed w/o the overhead comparable to a virtual.)

like image 43
lorro Avatar answered Mar 04 '26 23:03

lorro



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!