#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.) Тогда и компилятор поверит, что это чисто виртуальные классы (интерфейсы по-вашему), а не что-то странное абстрактное.
Комментариев нет:
Отправить комментарий