Search code examples
c++pure-virtual

C++ Interface with convenience methods


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?


Solution

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