Background
I am just trying to explore a bit of the available patterns and clever usage possibilities (in c++). There is a lot of information how to use multiple virtual inheritance or the pimpl idiom and there are nice topics about bridges or nested generalization but i was not able to find a good topic for this.
I am aware of (somehow) similar questions like:
but i think this is not a duplicate and worth it's own answer. My approach could be absolutely stupid, so please hint me into some helpful direction. This is not a homework and so there is not really any restriction on answers/languages/paradigms, but to not have a discussion about personal preference, please try to reason your answers why a specific pattern/solution is valid, either because other presented solutions are wrong/error prone/... .
Question
What would be a good way to implement the following structure (maybe composition instead of inheritance?). It is possible that different answers could be valid if reuse of code, easier to read/write, easier to extend is the main focus (maybe you can think of other problems?)
The main problem i see in the current implementation is that this is not extensable at all as we get one class for every combination of values for each property we want to model. For the sake of simplicity of the example i use a Camera with the properties Projection (either Ortographic or Perspective) and Behaviour (FirstPerson or FreeLook) but the idea could be the same for any entity A with properties P1,P2,P3,P4 which each can be one out of many subtypes P1=>P1.1,P1.2,... P2=>P2.1,P2.2,...
Projection shall define the implementation of
virtual const glm::mat4& getProjectionMatrix() = 0;
while Behaviour shall define the implementation of
virtual const glm::vec3& getForwardVector() = 0;
Possible Solution 1: Combine pimpl idiom with multiple virtual inheritance
Sidenote: I use the name _this for the pimpl pointer as _this->variable leads to very readable code from my point of view. Of course there is the danger of confusing this and _this in case there exists members with the same name (unlikely but indeed possible).
Camera.h
class Camera{
protected:
/*Pimpl Idiom extended by multiple virtual inheritance for the AbstractImpl subclasses*/
class AbstractImpl;
std::unique_ptr<AbstractImpl> _this;
/*Private subclasses for AbstractImpl*/
class Ortographic;
class Perspective;
class FirstPerson;
class FreeLook;
class OrtographicFirstPerson;
class OrtographicFreeLook;
class PerspectiveFirstPerson;
class PerspectiveFreeLook;
};
AbstractImpl.h
class Camera::AbstractImpl
{
public:
/*Also contains all private data members of class Camera (pimpl idiom)*/
virtual const glm::vec3& getForwardVector() = 0;
virtual const glm::mat4& getProjectionMatrix() = 0;
};
class Camera::Ortographic:virtual Camera::AbstractImpl{public:virtual const glm::mat4& getProjectionMatrix() override;};
class Camera::Perspective:virtual Camera::AbstractImpl{public:virtual const glm::mat4& getProjectionMatrix() override;};
class Camera::FirstPerson:virtual Camera::AbstractImpl{public:virtual const glm::vec3& getForwardVector() override;};
class Camera::FreeLook:virtual Camera::AbstractImpl{public:virtual const glm::vec3& getForwardVector() override;};
class Camera::OrtographicFirstPerson:virtual Camera::Ortographic,virtual Camera::FirstPerson{};
class Camera::OrtographicFreeLook:virtual Camera::Ortographic, virtual Camera::FreeLook{};
class Camera::PerspectiveFirstPerson:virtual Camera::Perspective, virtual Camera::FirstPerson{};
class Camera::PerspectiveFreeLook:virtual Camera::Perspective, virtual Camera::FreeLook{};
This obviously is not very useable in case of many properties or values per property but at first glance it looks like the code is reused very well as the both virtual methods are implemented exactly once per necessity. Also i think the code for the part of the subclasses of AbstractImpl is very nicely readable as it feels like assigning attributes. "I want this class to have this properties". Also all subclasses have full access to the datamembers and functions of AbstractImpl which might be necessary (let's say the return value of the overriden virtual method depends on values of the private data members).
Also this looks like an implementation which could be supported very well by code generators as you simply need correct inheritance for a new subclass.
Possible Solution 2:Composition
Another solution i could think of is just to use composition.
Camera.h
class Camera{
protected:
ProjectionType _projectionType;
CameraBehaviour _cameraBehaviour;
private:
class Impl;
std::unique_ptr<Impl> _this;
};
If the ProjectionType or CameraBehaviour sublcasses depend any values of Camera::Impl an instance would be necessary to pass and i think this would clutter the code a lot. On the pro side we end up with way less classes as we need one class per projectionType and one class per cameraBehaviour. This might be the solution to go if we have many possible values for type and behaviour.
Possible Solution 3: Inheritance
Of course we could just use subclasses of Camera. In this case the pimpl might or not might be used at all as all data members which require protected visibilty shouldn't be part of the pimpl anyway.
Camera.h
class AbstractCamera{
protected:
/*All protected data members*/
private:
/*Maybe pimpl idiom makes no sense here because most of the variables might be protected*/
class Impl;
std::unique_ptr<Impl> _this;
};
PerspectiveFreeLookCamera.h
class PerspectiveFreeLookCamera: virtual AbstractCamera{
/*Override both methods*/
};
PerspectiveFirstPersonCamera.h
class PerspectiveFirstPersonCamera: virtual AbstractCamera{
/*Override both methods*/
};
And the same for the Ortographic classes. Here the virtual methods would be implemented multiple times as all Perspective/Ortographic... classes share the same implementation for the method
virtual const glm::mat4& getProjectionMatrix() = 0;
While all FreeLook/FirstPerson classes share the same implementation for
virtual const glm::vec3& getForwardVector() = 0;
Thank you for taking your time, i hope this can be considered a good question as i have put quite some thought into it, such that it is interesting for hopefully many people :)
Edit: If someone can imagine a better title for this question, please feel free to edit it, i had quite a hard time finding a good title.
Possible Solution 4: Templates (Added 4.11 19:00 GMT+2)
Based on the answer of Yakk I have worked on a solution with templates.
Camera.h
class Camera
{
public:
/*Forward declaration of public classes of camera
of course it would be possible to use their own header files to have a stricter one class
per file nature but i do not really see the need for this*/
template<typename t_projection, typename t_behaviour>
class Impl;
class AbstractImpl;
Camera(AbstractImpl *impl);
~Camera();
/* All other public functions for camera*/
/* Example method we want to implement based on tags/traits */
const glm::mat4& getProjectionMatrix();
private:
std::unique_ptr<AbstractImpl> _this;
};
Camera.cpp
#include "Camera.h"
#include "Impl.h"
Camera::Camera(AbstractImpl *impl) : _this(impl){}
Camera::~Camera() = default;
/*And all implementations*/
/*PIMPL Facade function*/
const glm::mat4& Camera::getProjectionMatrix(){
return _this->getProjectionMatrix();
}
Impl.h
namespace Projection{
struct Ortographic{};
struct Perspective{};
}
namespace Behaviour{
struct FirstPerson{};
struct FreeLook{};
}
/* Abstract base class for all different implementations */
class Camera::AbstractImpl
{
public:
/* Contains all PIMPL-Idiom members of camera */
/* Contains the pure virtual function for our trait/tag-based method */
virtual const glm::mat4& getProjectionMatrix() = 0;
};
template<typename t_projection, typename t_behaviour>
class Camera::Impl : public Camera::AbstractImpl
{
public:
Impl(){}
const glm::mat4& getProjectionMatrix(){
return getProjectionMatrix(t_projection{});
}
const glm::mat4& getProjectionMatrix(Projection::Ortographic){
/* Code for Ortographic Projection */
}
const glm::mat4& getProjectionMatrix(Projection::Perspective){
/* Code for Perspective Projection */
}
};
And this can be use now in the way
Camera * c = new Camera(new Camera::Impl<Projection::Ortographic,Behaviour::FreeLook>());
c->getProjectionMatrix();
Benefit: This should be mostly statically typed beside the obvious dynamic typing of the AbstractImpl subclasses. It is possible to switch the concrete implementation of a camera during runtime. New Traits can be added very easily. It would be also possible to use this approach as all that is needed is adding subclasses of AbstractImpl.
Drawback: Debugging can become quite a pain. Alone during trying this out i realized, that altough many errors might show during compile/linking time it still is way more difficult to find the problems.
I would appreciat some feedback on solution 4 as well as i added this because of Yaaks implementation. Did i use your suggestion correctly? Did I oversee something or used templates wrongly - as i have not used them before.