Страницы

Поиск по вопросам

понедельник, 16 декабря 2019 г.

C++ Наследование интерфейсов и реализующих их классов

#cpp


Представим, что у меня есть кот, который, само собой, является животным:

class Animal {
public:
    void walk() { std::cout << "walking..."; }
    virtual void say() const = 0;
};

class Cat : public Animal {
public:
    void say() const override { std::cout << "meow"; }
    void purr() const override { std::cout << "purr..."; }
};


А ещё мой кот должен уметь ходить между мирами. То есть модулями. И для этого у него
должен быть интерфейс ICat, через который внешние модули его и используют. При этом
внешние модули могут не знать, как работать с котами, но уметь работать с животными.
А значит нужен и интерфейс IAnimal. Как бывший C# разработчик я сразу соорудил следующую
систему:

struct IAnimal {
public:
    virtual ~IAnimal() = default;
    virtual void walk() = 0;
    virtual void say() const = 0;
};

struct ICat : public virtual IAnimal {
public:
    virtual void purr() const = 0;
};

class Animal : public virtual IAnimal {
public:
    void walk() override { std::cout << "walking..."; }
};

class Cat : public Animal, public ICat {
public:
    void say() const override { std::cout << "meow"; }
    void purr() const override { std::cout << "purr..."; }
};


Моему разочарованию не было предела, когда в ответ я увидел warning: 

warning C4250: 'Cat' : inherits 'Animal::Animal::walk' via dominance.


Первый вариант, который приходит в голову: поскольку компилятор банально не понимает,
что я ни за что не начну переопределять walk в ICat (ведь ICat - это всего-лишь безвольный
интерфейс), то можно попросту поставить #pragma warning. И всё.

Ещё вариант, можно убрать наследование ICat от IAnimal. Это уберёт warning, но тогда
придётся делать бесконечные касты ICat к IAnimal. При этом глядя на доступное извне
описание интерфейса ICat, совершенно не очевидным является, что он может быть приведён
к IAnimal. И да, сейчас это очевидно из понятий кота и животного. В реальной же ситуации,
со сложными иерархиями, такой понятности можно не ждать. 

Можно "подсказать" о наличии такого каста, добавлением метода IAnimal ICat::asAnimal().
Это решает большинство вопросов, но до C#-повской простоты и понятности тут очень далеко.

Собственно вопрос: как это сделать правильно в C++?
    


Ответы

Ответ 1



Дело в том, что наследование в C++ не такое, как в C#. В частности, понятия «интерфейс» там вовсе нет, и вам пришлось эмулировать его при помощи множественного наследования. Таким образом, у вас действительно walk наследуется по двум путям. В случае, когда один из классов-предков (в вашем случае ICat) не перекрывает walk, то компилятор может «самостоятельно» выбрать реализацию в Cat. Но тем не менее, компилятор Visual Studio честно предупреждает о возможных проблемах (о них здесь и немного здесь), которые лежат не в плоскости кода, а в плоскости «ожидаемого» поведения. В вашем случае (эмуляция интерфейсов) эти проблемы не возникнут. Так что просто подавите это предупреждение (через #pragma warning) и программируйте дальше. Кстати, gcc ваш код компилирует без предупреждений.

Ответ 2



Во-первых, если код работает в разных модулях, то спрячьте деструктор интерфейса и никому его не показывайте. struct IAnimal { virtual void walk() = 0; virtual void say() const = 0; protected: virtual ~IAnimal() = default; }; Виртуальный деструктор нужен чтобы можно было написать IAnimal* a = ...; delete a; Но Вы не можете так делать, т.к. delete должен вызываться только в том модуле, где создали объект (иначе всё сломается когда модули будут использовать разные версии рантайма языка). Во-вторых в С++ такие интерфейсы не нужны. Всё будет работать без них. И виртуальные функции не нужны, но их можно использовать как замену dllexport/dllimport. // Foo.hpp class Foo { public: using UniqueFoo = unique_ptr; static LIB_EXPORT UniqueFoo create(); virtual Bar* add(const char* key); virtual void remove(const char* key); private: Foo() = default; ~Foo() = default; std::map> m_; }; // Foo.cpp auto Foo::create() -> UniqueFoo { return {new Foo, [](Foo* p){ delete p; }}; } Bar* Foo::add(const char* key) { m_.emplace(key, make_unique())->second.get(); } void Foo::remove(const char* key) { m_.erase(key); } Что может пойти не так? Ничего, если не передавать между модулями типы, реализация которых может отличаться. Реализация delete может быть разной - значит передаем unique_ptr со своим делетером. unique_ptr прост как пробка, его можно передать как есть. std::string может отличаться - значит передаем const char*. std::map может отличаться - но он член класса и мы его никуда не передаем. У класса может быть разный sizeof? Ну и ладно, он у нас создается фабрикой.

Ответ 3



struct ICat : public virtual IAnimal Зачем здесь виртуальность? Интерфейсы не несут реализаций, поэтому делить им вроде как нечего. (Непонятен смысл деструктора в IAnimal, ибо это уже относится к тонкостям реализации, т.е. иерархии Animal.) Тогда и компилятор поверит, что это чисто виртуальные классы (интерфейсы по-вашему), а не что-то странное абстрактное.

Комментариев нет:

Отправить комментарий