C++ Interface with convenience methods

92 views Asked by At

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?

1

There are 1 answers

0
Ted Lyngmo On BEST ANSWER

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();
    }
};