I'm writing plugins for an application through its C++ SDK. The mechanism is fairly simple. A plugin provides its functionality through predefined interfaces. This is done by having server classes inherit from one implementation class per interface, which contains either pure vitual functions or non-pure functions with default implementations.
This is very practical as SDK clients only have to override those methods that the plugin requires and/or provide an implementation for the (rare) ones with no default.
What has been bugging me is that everything is known at compile time. The virtual function tables and machinery associated with runtime polymorphism are here only for the sake of providing default implementations.
I'm attempting to remove this overhead while keeping the convenience.
As a (very contrived) example, say I have a couple of servers presenting a single interface (named Blah) consisting of only one method with no default implementation.
// SDK header
struct OldImpl_Blah {
virtual ~OldImpl_Blah() =default;
virtual int mult(int) =0;
};
// plugin source
class OldServer3 : public OldImpl_Blah {
public:
int mult(int i) override { return 3 * i; }
};
class OldServer5 : public OldImpl_Blah {
public:
int mult(int i) override { return 5 * i; }
};
For pure virtual functions, straight forward CRTP works just fine.
// SDK header
template <typename T>
struct NewImpl_Blah {
int mult(int i) { return static_cast<T*>(this)->mult(i); }
};
// plugin source
class NewServer3 : public NewImpl_Blah<NewServer3> {
public:
int mult(int i) { return 3 * i; }
};
class NewServer5 : public NewImpl_Blah<NewServer5> {
public:
int mult(int i) { return 5 * i; }
};
The problem is with non-pure virtual functions, i.e. when there is a default implementation for the method.
// SDK header
struct OldImpl_Blah {
virtual ~OldImpl_Blah() =default;
virtual int mult(int i) { return i; } // default
};
// plugin source
class OldServer3 : public OldImpl_Blah {
public:
int mult(int i) override { return 3 * i; }
};
class OldServer5 : public OldImpl_Blah {
public:
int mult(int i) override { return 5 * i; }
};
I tried to combine CRTP with some expression SFINAE trickery and failed.
I guess what I need is some kind of code dispatching where the base class would either provide a default implementation or forward its arguments to the implementation in the derived class, if it exists.
The problem seems to be that the dispatch should rely on information that is not yet available to the compiler in the base class.
A simple solution would be to just remove the virtual
and override
keywords in the code. But then the compiler wouldn't check that the function signatures match.
Is there some well known pattern for this situation? Is what I'm asking possible at all?
(Please use small words as my expertise with templates is a bit on the light side. Thanks.)
As always, Yet Another Level of Indirection is the solution. In this particular case, it's the well known technique of public non-virtual functions calling private or protected virtual functions. It have its own uses, independent of what is being discussed here, so check it out regardless. Normally it works like this:
struct OldImpl_Blah {
piblic:
virtual ~OldImpl_Blah() = default;
int mult(int i) { return mult_impl(i); }
protected:
virtual int mult_impl(int i) { return i; }
};
// plugin source
class OldServer3 : public OldImpl_Blah {
protected:
int mult_impl(int i) override { return 3 * i; }
};
With CRTP it's exactly the same:
template <class T>
struct OldImpl_Blah {
piblic:
virtual ~OldImpl_Blah() = default;
int mult(int i) { return static_cast<T*>(this)->mult_impl(i); }
protected:
virtual int mult_impl(int i) { return i; }
};
// plugin source
class OldServer3 : public OldImpl_Blah<OldServer3> {
protected:
int mult_impl(int i) override { return 3 * i; }
};
Disclaimer: CRTP is said to eliminate virtual call overhead by nit requiring functions to be virtual
. I don't know if CRTP has any performance benefits when functions are kept virtual
.
Consider using something like policy design:
struct DefaultMult {
int mult(int i) { return i; }
};
// SDK header
template <typename MultPolicy = DefaultMult>
struct NewImpl_Blah {
int mult(int i) { return multPolicy.mult(i); }
private:
MultPolicy multPolicy;
};
// plugin source
class NewServer3 {
public:
int mult(int i) { return 3 * i; }
};
class NewServer5 {
public:
int mult(int i) { return 5 * i; }
};
void client() {
NewImpl_Blah<NewServer5> myServer;
}
Also note that in theory using final
keyword with override
enables compilers to dispatch more optimally than vtable approach. I expect modern compilers to optimise if you use final
keyword in your first implementation.
Some helpful refs:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With