Reputation: 630
I'm working on making a wrapper to be able to port future code easily to different backend rendering engines. We are currently working in GDI. Currently I am implementing virtual functions on an abstract backend, but I'd like to change that to CRTP since the backend should be known at compile time.
Unfortunately one hiccup I've experienced with CRTP (first time using) is that I must implement all details of derived functions. In contrast, the abstract implementation does not require fully implemented derived children. To demonstrate consider this:
#include <Windows.h>
#include <iostream>
struct AbstractBackend
{
virtual ~AbstractBackend() = 0;
virtual void foo()
{
throw "implementation missing: failed to override in derived class";
}
virtual void bar()
{
throw "implementation missing: failed to override in derived class";
}
};
AbstractBackend::~AbstractBackend() {}
struct ConcreteBackendA : AbstractBackend
{
int backendResource;
ConcreteBackendA(int rsc) :
backendResource(rsc)
{}
virtual void foo()
{
printf("executing ConcreteBackendA::foo!\n");
}
// ConcreteBackendA does not support "bar" feature
};
struct ConcreteBackendB : AbstractBackend
{
HDC backendResource;
ConcreteBackendB(HDC hdc) :
backendResource(hdc)
{}
virtual void foo()
{
printf("executing ConcreteBackendB::foo!\n");
}
virtual void bar()
{
printf("executing ConcreteBackendB::bar!\n");
}
};
struct FrontEnd
{
AbstractBackend *backend;
FrontEnd(int rsc) :
backend(new ConcreteBackendA(rsc))
{}
FrontEnd(HDC hdc) :
backend(new ConcreteBackendB(hdc))
{}
~FrontEnd()
{
delete backend;
}
void foo()
{
backend->foo();
}
void bar()
{
backend->bar();
}
};
int main()
{
int rsc = 0;
HDC hdc = 0;
FrontEnd A(rsc);
FrontEnd B(hdc);
A.foo();
A.bar(); // throws an error, A::bar is not a feature of this engine
B.foo();
B.bar();
std::cin.get();
}
In this example, the AbstractBackend supports two features, foo & bar. The ConcreteBackendA only supports foo, bar is a function that it cannot support (maybe something like Draw3dText), but that's ok. The user can catch the exceptions and move on. One small drawback is the usage of virtual functions. I'd like to entertain the thought of using CRTP like this:
#include <Windows.h>
#include <iostream>
template <class Derived>
struct AbstractBackend
{
virtual ~AbstractBackend() = 0;
void foo()
{
static_cast<Derived*>(this)->foo();
}
void bar()
{
static_cast<Derived*>(this)->bar();
}
};
template <class Derived>
AbstractBackend<Derived>::~AbstractBackend() {}
struct ConcreteBackendA : AbstractBackend<ConcreteBackendA>
{
int backendResource;
ConcreteBackendA(int rsc) :
backendResource(rsc)
{}
void foo()
{
printf("executing ConcreteBackendA::foo!\n");
}
// ConcreteBackendA does not support "bar" feature
};
struct ConcreteBackendB : AbstractBackend<ConcreteBackendB>
{
HDC backendResource;
ConcreteBackendB(HDC hdc) :
backendResource(hdc)
{}
void foo()
{
printf("executing ConcreteBackendB::foo!\n");
}
void bar()
{
printf("executing ConcreteBackendB::bar!\n");
}
};
template <class ConcreteBackend>
struct FrontEnd
{
AbstractBackend<ConcreteBackend> *backend;
FrontEnd(int rsc) :
backend(new ConcreteBackendA(rsc))
{}
FrontEnd(HDC hdc) :
backend(new ConcreteBackendB(hdc))
{}
~FrontEnd()
{
delete backend;
}
void foo()
{
backend->foo();
}
void bar()
{
backend->bar();
}
};
int main()
{
int rsc = 0;
HDC hdc = 0;
FrontEnd<ConcreteBackendA> A(rsc);
FrontEnd<ConcreteBackendB> B(hdc);
A.foo();
A.bar(); // no implementation: stack overflow
B.foo();
B.bar();
std::cin.get();
}
The problem is that if a derived class fails to implemented a function from the AbstractBackend, then the AbstractBackend will call itself causing a stack overflow.
How can I replicated the behavior of the virtual abstract implementation with CRTP?
Upvotes: 2
Views: 383
Reputation: 275220
template <class Derived>
struct AbstractBackend
{
virtual ~AbstractBackend() = 0;
void foo()
{
static_cast<Derived*>(this)->foo_impl();
}
void bar()
{
static_cast<Derived*>(this)->bar_impl();
}
void foo_impl()
{
throw "implementation missing: failed to override in derived class";
}
void bar_impl()
{
throw "implementation missing: failed to override in derived class";
}
};
now you can have a default implementation of foo
/bar
.
Derived classes override foo_impl
instead of foo
.
However, this particular use is a bad plan; you know at compile time if a given AbstractBackend<D>
is implemented or not.
We are, after all, implementing compile-time "dynamic" dipatch; why not evaluate the error at compile time?
void foo_impl() = delete;
void bar_impl() = delete;
now, at the moment the dispatch is done in your code at compile time, you are given the error, instead of waiting until compile time.
Upvotes: 2
Reputation: 40060
You are abusing Object Oriented Programming.
Semantically, AbstractBackend
is an interface: a contract. If a class Alice
inherits from AbstractBackend
, then an Alice
is an AbstractBackend
. Not partially an AbstractBackend
. Fully an AbstractBackend
. This is the Liskov's substitution principle (the L of SOLID).
If classes Bob
and Charlie
partially implement AbstractBackend
, this means you really have two contracts: Interface1
and Interface2
:
Bob
implements (inherits) Interface1
,Charlie
implements (inherits) Interface2
,Alice
implements (inherits) Interface1
and Interface2
.CRTP is usable again, your code smells good and fresh, life is enjoyable. Have a great weekend.
Upvotes: 2