Suppose I have the following interface:
struct Person {
std::string name;
unsigned short age;
};
class ContainerInterface {
public:
virtual ~ContainerInterface () = default;
virtual void addPerson (Person const&) = 0;
virtual std::optional<Person> getPerson (std::string const& name) const = 0;
virtual bool hasPerson (std::string const& name) const = 0;
};
This interface could be implemented in the following way:
class Container: public ContainerInterface {
public:
virtual void addPerson (Person const& person) override {
_people.push_back(person);
}
virtual std::optional<Person> getPerson (std::string const& name) const override {
for (const auto& person: _people) {
if (person.name == name) return person;
}
return std::nullopt;
}
virtual bool hasPerson (std::string const& name) const override {
return getPerson(name).has_value();
}
private:
std::vector<Person> _people;
};
While this looks straightforward, there is a catch: methods like hasPerson
are really just an alias for getPerson(name).has_value()
. All implementations of ContainerInterface
should share that, it shouldn't be left to the implementation itself to enforce. In fact, nothing stops the implementation from doing something like this:
class BrokenContainer: public Container {
public:
virtual bool hasPerson (std::string const& name) const override {
return false;
}
};
Sure, I can fix this by implementing hasPerson
as part of ContainerInterface
:
class ContainerInterface {
public:
virtual ~ContainerInterface () = default;
virtual void addPerson (Person const&) = 0;
virtual std::optional<Person> getPerson (std::string const& name) const = 0;
virtual bool hasPerson (std::string const& name) const final;
};
// Should always do this, regardless of implementation
bool ContainerInterface::hasPerson (std::string const& name) const {
return getPerson(name).has_value();
}
But then my ContainerInterface
is not a pure interface anymore. In some production settings, there are macros that I could stick to my interface to mark it a pure interface, and that check that it doesn't implement any methods. If I use this approach I wouldn't be able to mark my class as a pure interface.
Another workaround would be to implement hasPerson
as a free function:
bool hasPerson (ContainterInterface const& container, std::string const& name) {
return container.getPerson(name).has_value();
}
But that doesn't feel as satisfactory, since hasPerson
sounds like it should be a method of ContainerInterface
.
Is there any more elegant way of keeping ContainerInterface
a pure interface while enforcing that all implementations share the same meaning to hasPerson
?
A workaround is to not let other classes inherit from ContainerInterface
directly.
class ContainerInterface { // pure
public:
virtual ~ContainerInterface() = default;
virtual void addPerson (Person const&) = 0;
virtual std::optional<Person> getPerson (std::string const& name) const = 0;
virtual bool hasPerson (std::string const& name) const = 0;
private:
ContainerInterface() = default; // private
friend class ContainerBase; // except for ContainerBase
};
class ContainerBase : public ContainerInterface {
public:
bool hasPerson (std::string const& name) const override final {
// ^^^^^
return getPerson(name).has_value();
}
};
class Container : public ContainerBase {
// ^^^^^^^^^^^^^
//...
};
This is essentially the same as giving the interface a default implementation that can't be overridden:
class ContainerInterface {
public:
virtual ~ContainerInterface() = default;
virtual void addPerson (Person const&) = 0;
virtual std::optional<Person> getPerson (std::string const& name) const = 0;
virtual bool hasPerson (std::string const& name) const final {
return getPerson(name).has_value();
}
};