C++ множественное наследование и спецификаторы доступа: наследование от базового класса и его производных

Это неожиданная проблема, с которой я столкнулся. Я пишу приложение с графическим интерфейсом и использую два класса из библиотеки графического интерфейса: класс Model и класс View. Содержимое модели визуализируется классом View на экране. В какой-то момент я решил вывести класс Model, потому что мне нужна была расширенная функциональность. Классы библиотеки должны быть производными, и я нашел много примеров. Это было легко и работало отлично.

Теперь есть проблема: класс Model предлагает методы для прямого редактирования данных модели. Я не хочу, чтобы эти методы были общедоступными, потому что я написал обертки, и эти обертки должны быть единственным способом редактирования. Мне нужно наследование, потому что моя производная MyModel переопределяет многие виртуальные методы класса Model. Так что я думал, что делать. Вот резюме со всеми подробностями:

  • Существует класс BasicModel, который предлагает методы для итерации данных модели.
  • Существует класс Model, который наследует BasicModel и расширяет его, также предлагая методы для редактирования данных (я думаю, что BaseModel является абстрактным и не имеет данных, а Model определяет внутренние структуры данных и реализует интерфейс итерации, предлагаемый BasicModel).
  • Есть класс MyModel, который наследует Model. Он переопределяет многие виртуальные методы, имеет расширенный механизм редактирования и хочет скрыть методы редактирования более низкого уровня, предлагаемые моделью.
  • Существует класс View, в котором хранится указатель на объект BasicModel. Таким образом, представление использует только интерфейс итерации и даже не знает о каких-либо интерфейсах редактирования.

Итак, я хочу оставить интерфейс итерации MyModel общедоступным, чтобы я мог передать его моему объекту View, но скрыть интерфейс редактирования, предлагаемый Model.

Решения, о которых я подумал:

Решение 1. Нельзя избежать наследования, потому что мне приходится переопределять виртуальные методы, но само решение не должно использовать наследование: я могу написать оболочку для класса MyModel, которая предлагает доступ к константная ссылка на инкапсулированный объект MyModel и предлагает оболочки для механизма редактирования MyModel. Это может быть уродливо из-за всех оберток, но позволяет избежать путаницы с множественным наследованием и спецификаторами доступа.

Решение 2. Пусть MyModel наследует Model. Отсутствие множественного наследования. Теперь перейдите к определению класса MyModel в MyModel.hpp и напишите объявления всех методов редактирования модели в разделах protected и private, чтобы они были скрыты. Это работает очень хорошо, но есть одна небольшая проблема с обслуживанием: если в будущих версиях библиотеки интерфейс модели изменится, например. добавлен новый метод редактирования, нам придется добавить его вручную в класс MyModel как закрытый/защищенный метод. Конечно, я могу отслеживать изменения в библиотеке или, по крайней мере, перейти к ее онлайн-справке по API и взглянуть на справочную страницу класса Model, просто чтобы убедиться, что ничего не изменилось, или обновить свой код, если это необходимо, перед рабочей/стабильной версией. мое приложение выпущено.

Решение 3. Используйте множественное наследование, и тогда я понятия не имею, что должно произойти. Это безопасно или нет. Зависит ли компилятор поведения или нет. Идея такова: MyModel наследует от Model и от basicModel: public наследование от BasicModel (для интерфейса итерации) и protected/private наследование от Model (чтобы скрыть интерфейс редактирования Model).

Примечания:

Примечание 1. Мой высокоуровневый механизм редактирования использует низкоуровневые методы редактирования Model.

Примечание 2. Виртуальные методы, которые MyModel переопределяет, некоторые из них определены в BasicModel (таким образом, также унаследованы от Model), а некоторые не существуют в BasicModel и определяются Model (например, методы, связанные с перетаскиванием). n-капля).

Примечание 3. Библиотека GUI, которую я использую, называется gtkmm, привязка C++ к GTK+, а классы, о которых я говорю, — Gtk:: TreeModel, Gtk::TreeStore, Gtk::TreeView и мой собственный класс MyModel, производный от Gtk::TreeStore. Я проигнорировал эти имена, потому что это вопрос общего планирования ООП, но я упоминаю здесь реальные классы, чтобы людям, которые с ними знакомы, было легче понять проблему.

Я не уверен, что здесь будет лучший дизайн. Определенно, решение 3 — это решение с наименьшими затратами на обслуживание. На самом деле это ноль, потому что спецификаторы доступа к наследованию просто выполняют всю работу автоматически. Вопрос в том, всегда ли решение 3 работает должным образом, например. для метода итерации компиляторы сделают его общедоступным (из-за открытого наследования от BasicModel) или закрытым (из-за частного наследования от модели, которая является производной от BasicModel). Я никогда не использовал множественное наследование, как это...

Псевдокод

Примерно так работает библиотека:

namespace GUI
{
     class BasicModel
     {
      public:
          /* iteration interface for observing the model */
          iterator get_iter();
          // but no data here, it's just an interface which calls protected virtual methods
          // which are implemented by deriving classes, e.g. Model
      protected:    
          virtual iterator get_iter_vfunc() = 0;
          virtual void handle_signal();
     };

     class Model : public BasicModel
     {
         /* inherits public iteration interface*/
         /* implements observation virtual methods from BasicModel*/
         virtual iterator get_iter_vfunc() { /*...*/ }
         /* has private data structures for the model data */
         std::vector<Row> data;
         /* has public editing interface, e.g. insert_row(), erase(), append()
         /* has protected drag-n-drop related virtual methods*/
         virtual void handle_drop();
     };
}

Мой код:

class MyModel : public GUI::Model
{
     /* implements virtual methods from BasicModel, e.g. default signal handlers*/
     virtual void handle_signal() { /*...*/ }
     /* implements drag-n-drop virtual methods from Model*/
     virtual void handle_drop() { *...* }
     /* inherits public iteration interface*/
     /* inherits public editing interface (***which I want to hide***)
     /* implements its own editing mechanism*/
     void high_level_edit (int param);
};

Поэкспериментируйте с GCC

Я попробовал следующий код, скомпилированный с отключенными предупреждениями (иначе GCC жалуется):

#include <iostream>

class Base
{
public:
    void func ()
    {
        std::cout << "Base::func() called" << std::endl;
        func_vfunc ();
    }
protected:
    virtual void func_vfunc ()
    {
        std::cout << "Base::func_vfunc() called" << std::endl;
    }
};

class Derived : public Base
{
protected:
    virtual void func_vfunc ()
    {
        std::cout << "Derived::func_vfunc() called" << std::endl;
    }
};

class MyClass : public Base, private Derived
{
};

int main (int argc, char* argv[])
{
    Base base;
    Derived derived;
    MyClass myclass;

    base.func ();
    derived.func ();
    myclass.func ();
    return 0;
}

По какой-то причине GCC настаивает на том, что вызов myclass.func() неоднозначен, но один из func() должен быть закрытым из-за частного наследования, поэтому я не понимаю, почему он не может скомпилироваться. Итог, если предположить, что это не ошибка, а просто я не понимаю, как все работает, - предлагаемое решение множественного наследования невозможно. Единственный способ решить эту проблему, если я не ошибаюсь, это виртуальное наследование, но я не могу его использовать, потому что классы, которые я использую, являются библиотечными классами и они не используют виртуальное наследование. И даже в этом случае, поскольку я использую частное и публичное наследование вместе, это может не решить проблему и по-прежнему вызывать неоднозначную реакцию.

РЕДАКТИРОВАТЬ: я попытался сделать Derived и MyClass практически производными от Base, и это полностью решает проблему. Но в моем случае я не могу изменить библиотечные классы, так что это не вариант.


person cfa45ca55111016ee9269f0a52e771    schedule 16.02.2013    source источник
comment
Решение 3 с минимальными затратами на обслуживание? Я сомневаюсь. Читайте о шаблонах проектирования, и наследование будет наименее предпочтительным   -  person exexzian    schedule 16.02.2013
comment
@sansix Model и BasicModel — это библиотечные классы, я их вообще не обслуживаю. И гарантируется, что BasisModel сохраняет интерфейс итерации, в то время как Model предлагает интерфейс редактирования, потому что так работает библиотека, это дизайн классов, и я не ожидаю, что он когда-либо изменится. Представление использует указатель BasicModel, так что я в безопасности. Да, учитывая эти детали, я ожидаю минимальных затрат на обслуживание. У меня нет опыта работы с множественным наследованием, но это не меняет фактов, упомянутых выше :)   -  person cfa45ca55111016ee9269f0a52e771    schedule 16.02.2013
comment
Вероятно, вам следует добавить некоторый код к вашему вопросу (упрощенные определения классов), чтобы мы могли лучше понять, в чем проблема, и избежать недоразумений (и я должен был начать с того, что спросил вас об этом, мой плохой снова).   -  person syam    schedule 16.02.2013
comment
@syam Я добавил псевдокод. Надеюсь понятно, старался максимально упростить   -  person cfa45ca55111016ee9269f0a52e771    schedule 16.02.2013
comment
Я добавил пример кода, который пытался скомпилировать   -  person cfa45ca55111016ee9269f0a52e771    schedule 16.02.2013


Ответы (2)


Если я правильно понимаю вашу проблему, вам, вероятно, следует использовать сочетание наследования (MyModel происходит от BaseModel) и композиции (MyModel содержит частный экземпляр модели).

Затем в MyModel используйте свою собственную реализацию базовых виртуальных методов, а для тех, которые вы не хотите переопределять, просто сделайте их прокси для соответствующих методов модели.

В любом случае, ИМХО, по возможности следует избегать множественного наследования. В противном случае со временем он может стать довольно волосатым.


РЕДАКТИРОВАТЬ: Теперь, когда я вижу код, я попробую еще раз.

Что вам нужно сделать, так это переопределить все, что вам нужно, в классе MyModelImpl (производном от модели), который будет скрыт внутри прокси-класса MyModel (производного от BaseModel). Моя первая идея была очень похожей, за исключением того, что я не понимал, что вам нужно переопределить некоторые части модели.

Что-то вроде:

class MyModel : public BaseModel {
public:
  void high_level_edit(int param) { m_impl.high_level_edit(param); }
protected:
  virtual iterator get_iter_vfunc() { return m_impl.get_iter_vfunc(); }
  virtual void handle_signal() { m_impl.handle_signal(); }

private:
  class MyModelImpl : public Model {
  public:
    void high_level_edit(int param);
    // reimplement whatever you need (methods present in BaseModel,
    // you need to call them from MyModel proxies)
    virtual iterator get_iter_vfunc() { /*...*/ }
  protected:
    // reimplement whatever you need (methods present only in Model,
    // you don't need to call them from MyModel proxies)
    virtual void handle_drop();
  };
  MyModelImpl m_impl;
};

Я считаю, что это должно работать правильно, поскольку в BaseModel нет фактического состояния (данных), если только я не опять что-то неправильно понял...

person syam    schedule 16.02.2013
comment
Виртуальные методы, которые я переопределяю, определяются моделью, поэтому я имею производные от модели. - person cfa45ca55111016ee9269f0a52e771; 16.02.2013
comment
Также мне лично не нравится этот дизайн, потому что интерфейс BasicModel существует дважды, в унаследованном классе и в инкапсулированном экземпляре. - person cfa45ca55111016ee9269f0a52e771; 16.02.2013
comment
@ fr33domlover, мой плохой, я понял, что везде, где модель используется библиотекой, она фактически используется только через ее интерфейс BaseModel. Что касается BasicModel, существующего дважды, как вы думаете, что произойдет, если вы будете использовать множественное наследование? ;) Если BaseModel не наследуется фактически от Model, у вас большие проблемы с множественным наследованием... - person syam; 16.02.2013
comment
Я мог бы использовать MyModel для виртуального наследования от Model и BasicModel. Кроме того, не будет двойного интерфейса, потому что один из них будет скрытым — унаследованным от Model — и только один, унаследованный от BasicModel, будет общедоступным, так что это не считается дважды. В любом случае, дело в том, что я должен наследовать модель, потому что мне нужно получить виртуальные методы (которых нет в BasicModel). - person cfa45ca55111016ee9269f0a52e771; 16.02.2013
comment
@fr33domlover, я отредактировал свой ответ. Это должно быть ближе к тому, что вам нужно: даже если вам нужны некоторые корректировки, вы поняли идею. - person syam; 16.02.2013
comment
Я понимаю ваше решение, но с ним есть проблема: мне нужно написать все виртуальные защищенные оболочки методов итерации. Это означает, что мне нужно обновить код при изменении библиотеки, поэтому в основном вы предлагаете решение 1, которое я предложил в вопросе. Я знаю, как это написать, вопрос в том, какое решение было бы лучше... в любом случае спасибо :) Я разобрался, проблема в том, что я хочу скрыть функциональность редактирования (инкапсулировать), но выставить функциональность итерации и расширить редактирование (вывод), и я не могу обойтись без какого-либо обходного пути, например, что вы предлагаете - person cfa45ca55111016ee9269f0a52e771; 16.02.2013
comment
Фактический ответ таков: если вы можете избежать сокрытия, избегайте его. В моем приложении есть функции более высокого уровня, которые вызывают высокоуровневые методы редактирования MyModel, поэтому сама MyModel скрыта от пользователя. Опасность заключается в том, что я случайно вызываю низкоуровневое редактирование, которое мне не следует использовать, но я могу проверить это и найти ошибки во время отладки, поэтому пользователь получает чистый код, и я избегаю большинства проблем. Так что осталось: перевести все это в ответ... - person cfa45ca55111016ee9269f0a52e771; 16.02.2013
comment
@ fr33domlover, да, действительно, теперь, когда вы это говорите, это похоже на ваше решение №1. Извините за это, мой английский иногда плохой, поэтому я не полностью понял, что вы сказали. Виноват. Дело в том, что я понимаю код лучше, чем английский... Извините, если я потратил ваше время впустую, все, что я надеюсь, это то, что моя бессвязность помогла вам провести мозговой штурм по вашему вопросу. Удачи. ;) - person syam; 17.02.2013

Вы можете сделать что-то вроде

class MyModel : protected Model

{

public:
    using Model::iterator;
    //it will make iterator public in MyModel
}

если я правильно понимаю случай и при условии, что модель не наследуется частным образом от итерируемого класса, что, я думаю, должно быть здесь. Простите меня, если я сказал что-то глупое. Я не очень опытный кодер.

person saga    schedule 04.11.2016