Наследование и интерфейсы в C++

Допустим, у меня есть полностью виртуальный/абстрактный класс INode (интерфейс), все его методы исключительно виртуальные. В последствии я хочу от него уже наследовать класс CNode, где эти методы будут реализованы. Также я хочу создать потомок-интерфейс, например INodeSpatial, тоже полностью виртуальный, который унаследует все виртуальные методы родителя + добавит какие-то свои виртуальные методы. В последствии я хочу от него наследовать CNodeSpatial где все эти методы будут реализованы. Ну и так далее..

Но тут получается следующее:

  1. Класс CNode, который наледуется от INode будет реализовывать вирутальные методы INode
  2. Класс CNodeSpatial, надедуется, опять же, только от виртуального INodeSpatial, но не от CNode, и это значит что в CNodeSpatial мне придется описывать все то же самое, что было описано в CNode
  3. Возникла мысль, чтобы этого не делать - наследовать CNodeSpatial и от INodeSpatial, и от CNode (где уже реализация всех виртуальных методов INode описана). Тогла мне придется описать только те виртуальные методы, которые есть в INodeSpatial

Но тут возникает вопрос:

Насколько это вообще хорошо и правильно? Просто получается что класс CNodeSpatial дважды наследуется от INode (поскольку INode является родителем и CNode и INodeSpatial). Так вообще можно? Не приведет ли это к проблемам в дальнейшем? Насколько такой подход соответствует "хорошей практике"?

Примеры:

Интерфейсы

class INode
{
public:
    virtual void methodA() = 0;
    virtual void methodB() = 0;
    virtual ~INode() = default;
};

class INodeSpatial : public INode
{
public:
    virtual void methodC(int foo) = 0;
    virtual void methodD(int foo) = 0;
    ~INodeSpatial() override = default;
};

"Реальные" классы:

class CNode : public INode
{
public:
    void methodA() override
    {
        //TODO: Implementation of INode methods in CNode
    }

    void methodB() override
    {
        //TODO: Implementation of INode methods in CNode
    }
};

class CNodeSpatial : public INodeSpatial
{
public:
    void methodA() override
    {
        //TODO: Implementation of INode methods in CNodeSpatial (already done in CNode)
    }

    void methodB() override
    {
        //TODO: Implementation of INode methods in CNodeSpatial (already done in CNode)
    }

    void methodC([[maybe_unused]] int foo) override
    {
        //TODO: Implementation of INodeSpatial methods
    }

    void methodD([[maybe_unused]] int foo) override
    {
        //TODO: Implementation of INodeSpatial methods
    }
};

Как можно видеть, в классе CNodeSpatial приходится дублировать реализации которые уже были описаны в CNode, потому что наследование идет исключительно от виртуальных интерфейсов. Но если наследоваться И от СNode, проблема как бы исчезает:

class CNodeSpatial : public INodeSpatial, public CNode
{
public:
    void methodC([[maybe_unused]] int foo) override
    {
        //TODO: Implementation of INodeSpatial methods
    }

    void methodD([[maybe_unused]] int foo) override
    {
        //TODO: Implementation of INodeSpatial methods
    }
};

Но в итоге получается то класс как бы 2 раза наследуется от INode, что меня и смущает. Это нормально? Так делают? Проблем в дальнейшем не вызовет?

введите сюда описание изображения


Ответы (2 шт):

Автор решения: HolyBlackCat

класс как бы 2 раза наследуется от INode

Чутье вас не подвело.

Ваш пример с class CNodeSpatial : public INodeSpatial, public CNode не делает то, что вы думаете.

Класс CNodeSpatial при таком подходе абстрактный - попробуйте создать экземпляр, увидите.

Решение - CNode и INodeSpatial должны наследоваться от INode виртуально.

→ Ссылка
Автор решения: AR Hovsepyan

Можно сделать так:

class INodeSpatial : public INode
{
protected:
    virtual void methodC(int foo) {}
    virtual void methodD(int foo) {}
public:
    
    ~INodeSpatial() override = default;
};

Так как базовый класс абстрактный, то и производный класс останется абстрактным, поскольку не определяет чистый виртуальный метод. Но он добавляет новые, не чистые, виртуальные методы, но только защищенные, чтобы производные классы могли переопределить и могли не переопределять. Практически INodeSpatial является типичным узловым классом для построения производных классов:

class CNode : public  INodeSpatial
{
public:
    void methodA() override
    {
        
        //TODO: Implementation of INode methods in CNode
    }

    void methodB() override
    {
       
        //TODO: Implementation of INode methods in CNode
    }
};

Теперь CNode имеет в своем открытом интерфейсе только методы из INode

class CNodeSpatial : public CNode
{
public:    

    void methodC([[maybe_unused]] int foo) override
    {
       
        //TODO: Implementation of INodeSpatial methods
    }

    void methodD([[maybe_unused]] int foo) override
    {
   
        //TODO: Implementation of INodeSpatial methods
    }
};

Ну а этот определил еще и методы из INodeSpatial, и имена типов соответствующие иерархии.

Хочется отметить, что я полностью согласен с самым первым комментарием под вопросом(от avp). Старайтесь делать проще. Например, если нет необходимости создавать дерево иерархии(всего два класса у вас фигурируют в данном виде) так, чтобы было удобно писать рудиментарный код по ссылке на базовый класс, то стоит подумать об избавлении всего лишнего...

→ Ссылка