How to create many classes to act like implementer for an interface class, while avoid v-table cost as possible, and still enable static casting to the interface?
For a simple case, it can be achieved like in the below example.
Library Code :-
class I{ //interface
public: virtual void i1()=0;
};
template<class Derived>class Router : public I{
public: virtual void i1()final{
//in real case it is very complex, but in the core is calling :-
static_cast<Derived*>(this)->u1();
}
};
User Code :-
class User : public Router<User>{
public: void u1(){ std::cout<<"hi"<<std::endl; }
};
int main() {
User u;
u.i1(); //<-- no v-table cost
I* i=&u;
i->i1(); //<-- has v-table cost (OK)
}
Full demo
How to extend the above feature to support 2 routes or more?
The below code is uncompilable, but it depicts my dream. (full demo).
Library Code :-
class I{ //interface
public: virtual void i1()=0;
public: virtual void i2()=0;
};
template<class Derived>class RouterI1U1 : public I{
public: virtual void i1()final{ static_cast<Derived*>(this)->u1(); }
};
template<class Derived>class RouterI1U2 : public I{
public: virtual void i1()final{ static_cast<Derived*>(this)->u2(); }
};
template<class Derived>class RouterI2U1 : public I{
public: virtual void i2()final{ static_cast<Derived*>(this)->u1(); }
};
template<class Derived>class RouterI2U2 : public I{
public: virtual void i2()final{ static_cast<Derived*>(this)->u2(); }
};
User Code :-
The one who want to use the above library, can easily pick whatever "route" he want.
RouterI1U2<User>
and RouterI2U1<User>
orRouterI1U1<User>
and RouterI2U2<User>
orRouterI1U1<User>
or RouterI1U2<User>
} and implement i2()
with final manually orRouterI2U2<User>
or RouterI2U1<User>
} and implement i1()
with final manually ori1()
and i2()
with final manuallyHere is an dreamed example of usage.
class User : public RouterI1U2<User>,public RouterI2U1<User>{
public: void u1(){ std::cout<<"hi1"<<std::endl; }
public: void u2(){ std::cout<<"hi2"<<std::endl; }
};
int main() {
User u;
u.i1(); //<-- no v-table cost
I* i=&u;
i->i1(); //<-- has v-table cost (OK)
}
class I{ //interface
public: virtual void i1()=0;
public: virtual void i2()=0;
};
template<class Derived> class RouterI1U2_I2U1 : public I{ //group it
public: virtual void i1()final{ static_cast<Derived*>(this)->u2(); }
public: virtual void i2()final{ static_cast<Derived*>(this)->u1(); }
};
class User : public RouterI1U2_I2U1<User>{
public: void u1(){ std::cout<<"hi1"<<std::endl; }
public: void u2(){ std::cout<<"hi2"<<std::endl; }
};
It works (demo), but offer less modularity. (low re-usability)
I have to pack RouterI1U2
and RouterI2U1
to RouterI1U2_I2U1
manually.
It may not apply in your case, but could be useful to other reading the question as well.
I suggest you to use the concept-model idiom for this particular case. The goal of this is to separate the polymorphism implementation and the implementation of those class themselves into different part. Here, I
becomes a polymorphic wrapper around any class that has i1
and i2
member functions:
class I {
// The interface is internal, invisible to outside
// We use this as a type erasure technique and polymorphism
struct Concept {
virtual void i1() = 0;
virtual void i2() = 0;
};
// The single implementation that directly
// extend the interface is the model. T is the user class.
// T must have i1 and i2 function, because we call them.
template<typename T>
struct Model : Concept {
// The user class.
// If you want, you can use a reference there if you
// need references semantics with I
T user;
Model (T u) : user{std::move(u)} {}
// The only implementation of i1 is to call i1 from the user class
void i1() override {
user.i1();
}
void i2() override {
user.i2();
}
};
// Or use a shared, or use SBO
std::unique_ptr<Concept> concept;
public:
// When we make an I, we must provide a user class.
// If the user class had i1 and i2, it will compile.
// If Model takes a reference, use a reference there too.
template<typename T>
I(T model) : concept{std::make_unique<Model<T>>(std::move(model))} {}
void i1() {
concept->i1();
}
void i2() {
concept->i2();
}
};
Then, your classes that provide implementations becomes like this:
template<class Derived>
struct RouterI1U1 { // no Inheritance needed
void i1() { static_cast<Derived*>(this)->u1(); }
};
template<class Derived>
struct RouterI1U2 {
void i1() { static_cast<Derived*>(this)->u2(); }
};
template<class Derived>
struct RouterI2U1 {
void i2() { static_cast<Derived*>(this)->u1(); }
};
template<class Derived>
struct RouterI2U2 {
void i2() { static_cast<Derived*>(this)->u2(); }
};
Since these i1
and i2
only needs to be "there" in order to fit into the Model<T>
class, no overriding is needed, so no virtual inheritance.
It would effectively look like this when used:
struct User : RouterI2U2<User> {
void i1() {}
void u2() {}
};
As you can see, we don't have any virtual method. The polymorphism is an implementation detail of I
. Since this class have all required member functions, the I
class will allow it.
And using the class I
too is quite simple. Let User2
be another user class that fits I
requirements:
User2 user2;
user2.i1(); // no vtable, so no vtable overhead possible
I myI{user2}; // works!
myI.i2(); // calls u2, with vtable
std::vector<I> v;
v.emplace_back(User2{});
v.emplace_back(User{}); // simple heh?
Here's how you can remove the router classes, and implement this thing with an "or" style interface. I mean an interface that allows you to implement something or something else.
In the Model<T>
class, you can check if i1
and i2
exists. If there are not existing, you can provide an implementation that calls u1
and u2
instead.
We start that by making type traits that can tell us if a particular type T
has the member function i1
or i2
:
template<typename...>
using void_t = void;
template<typename, typename>
struct has_i1 : std::false_type {};
template<typename T>
struct has_i1<T, void_t<decltype(std::declval<T>().i1())>> : std::true_type {};
template<typename, typename>
struct has_i2 : std::false_type {};
template<typename T>
struct has_i2<T, void_t<decltype(std::declval<T>().i2())>> : std::true_type {};
Now, we can change our model implementation to call u1
or u2
if i1
and i2
are not there:
template<typename T>
struct Model : Concept {
T user;
Model(T u) : user{std::move(u)} {}
void i1() override {
i1_helper(user);
}
void i2() override {
i2_helper(user);
}
private:
template<typename U>
auto i1_helper(U& u) -> std::enable_if_t<has_i1<U>::value> {
// Call i1 if has i1
u.i1();
}
template<typename U>
auto i1_helper(U& u) -> std::enable_if_t<!has_i1<U>::value> {
// Call u1 if has not i1
u.u1();
}
template<typename U>
auto i2_helper(U& u) -> std::enable_if_t<has_i2<U>::value> {
// Call i2 if has i2
u.i2();
}
template<typename U>
auto i2_helper(U& u) -> std::enable_if_t<!has_i2<U>::value> {
// Call u2 if has not i2
u.u2();
}
};
Now, your user classes are the simplest possible.
struct User1 {
void i1() {}
void i2() {}
};
struct User2 {
void i1() {}
void u2() {}
};
struct User3 {
void u1() {}
void i2() {}
};
struct User4 {
void u1() {}
void u2() {}
};
Use virtual inheritance.
template<class Derived>class RouterI1U1 : public virtual I{
etc. makes the code compilable.
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