Recently, we had a discussion about code using protected inheritance in order to hide the fact (to client code) a class inherits from a specific base class but to exploit this fact in the implementation.
The following code demonstrates this. It compiles with recent versions of GCC and clang++ (it uses C++11 features):
#include <vector>
#include <iostream>
class IObserver
{
public:
virtual void update() = 0;
};
class Model
{
std::vector<IObserver*> m_observers;
int m_number = 0;
public:
void addObserver(IObserver& observer) {
m_observers.push_back(&observer);
}
void setNumber(int value) {
m_number = value;
notifyObservers();
}
int number() const {
return m_number;
}
protected:
void notifyObservers() {
for (auto pObserver : m_observers)
pObserver->update();
}
};
// We want to hide the fact class 'View' has 'IObserver' interface.
class View : protected IObserver
{
Model* m_pModel;
public:
View(Model& model) : m_pModel(&model) {
model.addObserver(*this); // Exploit the fact we are an 'IObserver'.
}
protected:
void update() override {
std::cout << m_pModel->number() << std::endl;
}
};
int main(int argc, char *argv[])
{
Model model;
View view(model);
//view.update(); // ERROR: 'update' is a protected member of 'View'.
model.setNumber(1);
model.setNumber(2);
}
The 'View' class inherits 'IObserver' interface but uses 'protected' modifier. So correspondingly, the public method 'update' inherited from that interface is protected. In the constructor, class 'View' adds itself as an observer to the 'Model' instance passed in as a parameter. Running the executable will output '1' and '2' in two separate lines, so the code runs as expected.
Now this solution was intensely discussed in our team but in the end there were some questions we could not answer with a common agreement:
- Is this code actually legal w.r.t. to the C++ standard? The issue here is that, in the constructor, the 'View' class passes a reference to itself to method 'Model::addOberver' which expects a reference to an 'IObserver' instance with a public 'update' method. However, as the 'View' class uses protected inheritance, this method now is protected. So when 'Model::notifyObservers' method is called within 'Model::setNumber' method, the View's 'update' method will be called "from outside" in spite of the fact it is actually protected. (The C++ FAQ Lite states: "[protected inheritance] allows derived classes of the protected derived class to exploit the relationship to the protected base class". So this sounds like the code above is a corresponding use case.)
- Assumed it is legal code, is it also good design? The fact we discussed a lot and didn't come to a common agreement might be a hint that it is not. Some of our colleagues had the opinion that a class' interface is solely defined by its public methods. And if 'View' class wants to pass a reference to itself to a method expecting an 'IObserver' instance it must use public inheritance. Some others (including myself) didn't agree with this strict definition. The implementation knows about the fact it has (and can provide) an 'IObserver' interface, so why should it not exploit this knowledge and expose the protected interface to code it wants to? In C++ we are able to define both, a public interface for "normal" client code and a protected interface for client code that wants to specialize/extend the class. So from this point of view, a class' interface is defined by its public and its protected interface. It just depends on which kind of client code "looks at" a class' interface.
- If the goal is to hide away 'IObserver' interface from the public, is using the pImpl idiom solely for this purpose worth it? Because we were not able to come to a common conclusion regarding the first two questions we decided to "workaround" them by using the pImpl idiom and public inheritance in the implementation class. So we now have a class 'View' that does not inherit from 'IObserver' class at all. Instead, a class 'ViewImpl' now inherits from 'IObserver' publicly. While we agreed that this is "good design" it made our implementation more complex, we have one indirection more, and we now have to maintain nearly twice as much code. Furthermore, we need to maintain inheritance hierarchies for both, the public and the implementation classes. (Of course, these are well-known drawbacks from using the pImpl idiom.)
I appreciate your opinions regarding these three questions a lot!!! So many thanks in advance :-)