Проблема хрупкого базового класса
От: _hum_ Беларусь  
Дата: 05.11.15 20:46
Оценка:
Раньше считал, что если все непубличные члены класса имеют тип доступа private, то наследники автоматически будут защищены от всяких неприятностей (типа проблем, вытекающих из нарушения принципа Лисков и подобных им). А, оказывается то, нет. Оказывается, наличие виртуальных функций и в этом случае может все сломать:
//////////////////////////
 ////      Super       ////
 //////////////////////////
 class Super 
 {

  int m_counter = 0;

public:
  //-------------------
  void inc1(void) 
  {
     inc2();//++m_counter;
  }
  //-------------------
  virtual
    void inc2(void) 
  {
     m_counter+= 1;
  }
  //-------------------
};
 //////////////////////////

 //////////////////////////
 ////       Sub        ////
 //////////////////////////
class Sub : public Super 
{
  //-------------------
  virtual
    void inc2() override
  {
     inc1();
  }
  //-------------------

};
//////////////////////////


Получается, в базовом классе надо с крайней осторожностью использовать виртуальные функции "для своих нужд" (Странно, что об этом так мало говорят).
Re: Это проблема незакрытой рекурсии
От: T4r4sB Россия  
Дата: 05.11.15 20:54
Оценка:
Это проблема незакрытой рекурсии
Re[2]: Это проблема незакрытой рекурсии
От: _hum_ Беларусь  
Дата: 05.11.15 21:24
Оценка:
Здравствуйте, T4r4sB, Вы писали:

TB>Это проблема незакрытой рекурсии


дык, механизм виртуальных функций и есть реализация в с++ незакрытой рекурсии. разве нет?


кстати, вот нашел список правил, как избежать этих проблем (странно, что о них первый раз слышу):

this discipline has been formalized by C. Ruby and G. T. Leavens; it basically consists of the following rules:[7]

— No code invokes public methods on this.
— Code that can be reused internally (by invocation from other methods of the same class) is encapsulated in a protected or private method; if it needs to be exposed directly to the users as well, then a wrapper public method calls the internal method.
— The previous recommendation can be relaxed for pure methods.

Re: Проблема хрупкого базового класса
От: _Artem_ Россия  
Дата: 06.11.15 02:45
Оценка:
Здравствуйте, _hum_, Вы писали:

__>Получается, в базовом классе надо с крайней осторожностью использовать виртуальные функции "для своих нужд" (Странно, что об этом так мало говорят).


Автор, дело в способе использования виртуальных методов. Если ты хочешь чтобы какой-то функционал мог быть перекрыт наследником то ты делаешь функцию виртуальной. Но тебе не обязательно делать такую общую возможность. Это же определенный класс и у него определенная задача. Именно так и нужно воспринимать виртуальные функции — кастомизация поведения в наследнике, а не полностью изменение функционала на 180 градусов.
Re: Проблема хрупкого базового класса
От: jazzer Россия Skype: enerjazzer
Дата: 06.11.15 03:26
Оценка: +2
Здравствуйте, _hum_, Вы писали:

__>Получается, в базовом классе надо с крайней осторожностью использовать виртуальные функции "для своих нужд" (Странно, что об этом так мало говорят).


Конечно же, говорят, и решение давно известно, просто ты не там ищешь. Гугли NVI
jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
Re: Проблема хрупкого базового класса
От: LaptevVV Россия  
Дата: 06.11.15 06:13
Оценка:
Герб Саттер. NVI у его описан — jazzer напомнил, спасибо.
Хочешь быть счастливым — будь им!
Без булдырабыз!!!
Re[2]: link
От: CEMb  
Дата: 06.11.15 07:09
Оценка: +2
http://rsdn.ru/forum/cpp/1873634.flat#1873634
Автор: minorlogic
Дата: 29.04.06
taskbar organizer
Re[2]: Проблема хрупкого базового класса
От: -n1l-  
Дата: 06.11.15 07:24
Оценка: +1
Где именно?
Re[3]: Это проблема незакрытой рекурсии
От: T4r4sB Россия  
Дата: 06.11.15 07:53
Оценка:
Здравствуйте, _hum_, Вы писали:

__>Здравствуйте, T4r4sB, Вы писали:


TB>>Это проблема незакрытой рекурсии


__>дык, механизм виртуальных функций и есть реализация в с++ незакрытой рекурсии. разве нет?


Чего-чего, я не понял ничего.
Re[2]: Проблема хрупкого базового класса
От: _hum_ Беларусь  
Дата: 06.11.15 08:10
Оценка: :)
Здравствуйте, jazzer, Вы писали:

J>Здравствуйте, _hum_, Вы писали:


__>>Получается, в базовом классе надо с крайней осторожностью использовать виртуальные функции "для своих нужд" (Странно, что об этом так мало говорят).


J>Конечно же, говорят, и решение давно известно, просто ты не там ищешь. Гугли NVI


в статье вики по NVI сказано, что этот подход тоже не защищен от FCP (fragile class problem):

Using the NVI idiom may lead to fragile class hierarchies if proper care is not exercised. As described in [1], in Fragile Base Class (FBC) interface problem, subclass's virtual functions may get accidentally invoked when base class implementation changes without notice.
[...]
The solution is to observe a strict coding discipline of invoking exactly one private virtual extension point in any public non-virtual interface of the base class. However, the solution depends on programmer discipline and hence difficult to follow in practice.

More C++ Idioms/Non-Virtual Interface

Re[4]: Это проблема незакрытой рекурсии
От: _hum_ Беларусь  
Дата: 06.11.15 08:15
Оценка:
Здравствуйте, T4r4sB, Вы писали:

TB>Здравствуйте, _hum_, Вы писали:


__>>Здравствуйте, T4r4sB, Вы писали:


TB>>>Это проблема незакрытой рекурсии


__>>дык, механизм виртуальных функций и есть реализация в с++ незакрытой рекурсии. разве нет?


TB>Чего-чего, я не понял ничего.


ну, я говорю, что механизм виртуальных функций (позднего связывания) это же и есть в точности незакрытая рекурсия:

The dispatch semantics of this, namely that method calls on this are dynamically dispatched, is known as open recursion, and means that these methods can be overridden by derived classes or objects.

потому ваше замечание о том, что это проблема незакрытой рекурсии выглядит тавтологией (по отношению к моему высказыванию, что это проблема виртуальных функций).
Re[2]: Проблема хрупкого базового класса
От: _hum_ Беларусь  
Дата: 06.11.15 08:18
Оценка:
Здравствуйте, _Artem_, Вы писали:

_A_>Здравствуйте, _hum_, Вы писали:


__>>Получается, в базовом классе надо с крайней осторожностью использовать виртуальные функции "для своих нужд" (Странно, что об этом так мало говорят).


_A_>Автор, дело в способе использования виртуальных методов. Если ты хочешь чтобы какой-то функционал мог быть перекрыт наследником то ты делаешь функцию виртуальной. Но тебе не обязательно делать такую общую возможность. Это же определенный класс и у него определенная задача. Именно так и нужно воспринимать виртуальные функции — кастомизация поведения в наследнике, а не полностью изменение функционала на 180 градусов.


похоже на "у хорошего программиста и goto нормально работают"
Re[2]: Проблема хрупкого базового класса
От: _hum_ Беларусь  
Дата: 06.11.15 08:33
Оценка:
Здравствуйте, LaptevVV, Вы писали:

LVV>Герб Саттер. NVI у его описан — jazzer напомнил, спасибо.


см. выше — NVI не решает проблемы хрупкого базового класса
Re[3]: Проблема хрупкого базового класса
От: LaptevVV Россия  
Дата: 06.11.15 08:47
Оценка:
N>Где именно?
Книжка "Новые сложные задачи на С++": http://www.ozon.ru/context/detail/id/2342923/
Автор(ы): Герб Саттер
Издательство: Вильямс
Цена: 230р.

Данная книга представляет собой продолжение вышедшей ранее книги Решение сложных задач на C++ . В форме задач и их решений рассматриваются современные методы проектирования и программирования на C++. В книге сконцентрирован богатый многолетний

Задача 18. Виртуальность.
Цитирую:

Назовем этот шаблон проектирования шаблоном невиртуального интерфейса (Nonvirtual Intrface (NVI) pattern).

Далее код примера 18-2 по паттерну NVI
Хочешь быть счастливым — будь им!
Без булдырабыз!!!
Re[3]: Проблема хрупкого базового класса
От: jazzer Россия Skype: enerjazzer
Дата: 06.11.15 14:43
Оценка: +1
Здравствуйте, _hum_, Вы писали:

__>в статье вики по NVI сказано, что этот подход тоже не защищен от FCP (fragile class problem):

__>

__>Using the NVI idiom may lead to fragile class hierarchies if proper care is not exercised. As described in [1], in Fragile Base Class (FBC) interface problem, subclass's virtual functions may get accidentally invoked when base class implementation changes without notice.
__>[...]
__>The solution is to observe a strict coding discipline of invoking exactly one private virtual extension point in any public non-virtual interface of the base class. However, the solution depends on programmer discipline and hence difficult to follow in practice.

__>More C++ Idioms/Non-Virtual Interface


Ну, вообще-то, да, программирование требует дисциплины — это новость для кого-то?

API всегда надо продумывать, API это системы, компонента, или класса.

Я виртуальные вызовы визуализирую для себя как посылку сообщений по сети в распределенной системе, на которые другая сторона как-то реагирует и что-то присылает в ответ (в каком-то смысле, это отвечает изначальной формулировке ООП, когда объекты обмениваются сообщениями — просто надо это распространить и на общение базового и производного класса).

Так что, очевидно, ты не можешь вносить изменения в сервер, которые меняют протокол его общения с другим сервером, и ожидать, что ничего не сломается.
Так что когда говоря, что типа безопасные изменения в базовом классе ломают наследников — это не безопасные изменения, если они меняют протокол общения.
Все примеры, которые тут приводились (в т.ч. по ссылкам) — все об изменении протокола. Нужно всего лишь думать об этом, когда вносишь изменения: меняешь ты этими изменениями протокол или нет.
Вот и все.
Ничего запредельного тут нет.
Для любого, кто писал распределенные приложения, это азы и этот анализ делается спинным мозгом.
Нужно просто начать думать об этом во время работы с иерархиями наследования тоже.
jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
Re[4]: Проблема хрупкого базового класса
От: _hum_ Беларусь  
Дата: 06.11.15 16:04
Оценка:
Здравствуйте, jazzer, Вы писали:

J>Здравствуйте, _hum_, Вы писали:


__>>в статье вики по NVI сказано, что этот подход тоже не защищен от FCP (fragile class problem):

__>>

__>>Using the NVI idiom may lead to fragile class hierarchies if proper care is not exercised. As described in [1], in Fragile Base Class (FBC) interface problem, subclass's virtual functions may get accidentally invoked when base class implementation changes without notice.
__>>[...]
__>>The solution is to observe a strict coding discipline of invoking exactly one private virtual extension point in any public non-virtual interface of the base class. However, the solution depends on programmer discipline and hence difficult to follow in practice.

__>>More C++ Idioms/Non-Virtual Interface


J>Ну, вообще-то, да, программирование требует дисциплины — это новость для кого-то?



речь шла о том, что не удается придумать свод правил, рутинное выполнение которых гарантировало бы отсутствие эффекта хрупкого базового класса.


J>API всегда надо продумывать, API это системы, компонента, или класса.


J>Я виртуальные вызовы визуализирую для себя как посылку сообщений по сети в распределенной системе, на которые другая сторона как-то реагирует и что-то присылает в ответ (в каком-то смысле, это отвечает изначальной формулировке ООП, когда объекты обмениваются сообщениями — просто надо это распространить и на общение базового и производного класса).


J>Так что, очевидно, ты не можешь вносить изменения в сервер, которые меняют протокол его общения с другим сервером, и ожидать, что ничего не сломается.


извиняюсь, что вы понимаете под "протоколом". вот давайте, для конкретного примера:

class Set 
{
    std::set<int> s_;
public:
    //---------------------
    void add (int i) 
    {
      s_.insert (i);
      add_impl (i); // Note virtual call.
    }
    //---------------------
    void addAll (int * begin, int * end)
    {
      s_.insert (begin, end);   //  --------- (1)
      addAll_impl (begin, end); // Note virtual call.
    }
    //---------------------
private:
    virtual void add_impl (int i) = 0;
    virtual void addAll_impl (int * begin, int * end) = 0;
};


class CountingSet : public Set 
{
private:
    int count_;
    //---------------------
    virtual void add_impl (int i) 
    {
      count_++;
    }
    //---------------------
    virtual void addAll_impl (int * begin, int * end) 
    {
      count_ += std::distance(begin,end);
    }
    //---------------------
};


что здесь, по-вашему, относится к протоколу (общения между Set и CountingSet), который "не должен меняться"?


J>Так что когда говоря, что типа безопасные изменения в базовом классе ломают наследников — это не безопасные изменения, если они меняют протокол общения.

J>Все примеры, которые тут приводились (в т.ч. по ссылкам) — все об изменении протокола. Нужно всего лишь думать об этом, когда вносишь изменения: меняешь ты этими изменениями протокол или нет.
J>Вот и все.
J>Ничего запредельного тут нет.
J>Для любого, кто писал распределенные приложения, это азы и этот анализ делается спинным мозгом.
J>Нужно просто начать думать об этом во время работы с иерархиями наследования тоже.

эка лихо вы меняете позицию с признание проблемы в первом своем сообщении и низведение ее до надуманной теперь, когда выяснилось, что ваша точка зрения на ее решенность оказалась ошибочной.
Re[5]: Проблема хрупкого базового класса
От: jazzer Россия Skype: enerjazzer
Дата: 06.11.15 17:43
Оценка:
Здравствуйте, _hum_, Вы писали:

__>речь шла о том, что не удается придумать свод правил, рутинное выполнение которых гарантировало бы отсутствие эффекта хрупкого базового класса.


Я только что их привел. Или они не достаточно рутинные?

J>>API всегда надо продумывать, API это системы, компонента, или класса.


J>>Я виртуальные вызовы визуализирую для себя как посылку сообщений по сети в распределенной системе, на которые другая сторона как-то реагирует и что-то присылает в ответ (в каком-то смысле, это отвечает изначальной формулировке ООП, когда объекты обмениваются сообщениями — просто надо это распространить и на общение базового и производного класса).


J>>Так что, очевидно, ты не можешь вносить изменения в сервер, которые меняют протокол его общения с другим сервером, и ожидать, что ничего не сломается.


__>извиняюсь, что вы понимаете под "протоколом". вот давайте, для конкретного примера:

__>что здесь, по-вашему, относится к протоколу (общения между Set и CountingSet), который "не должен меняться"?

в этом примере протокол очень простой:
1) вызов add приводит к посылке производному классу сообщения add_impl
2) вызов addAll приводит к посылке производному классу сообщения addAll_impl

А изменение строчки (1) приводит к изменению второго пункта:
2) вызов addAll приводит к посылке производному классу N сообщений add_impl и затем сообщения addAll_impl.

Это именно изменение протокола. Если думать о взаимодействии базового и производного класса в терминах протокола, то все становится очевидно.
И NVI этот протокол четко специфицирует — за счет того, что нет виртуальных входов, вход только через NVI.

__>эка лихо вы меняете позицию с признание проблемы в первом своем сообщении и низведение ее до надуманной теперь, когда выяснилось, что ваша точка зрения на ее решенность оказалась ошибочной.


Проблема решается в терминах протокола. NVI этот протокол прибивает гвоздями — потому что есть только сценарии, заданные NVI, их никто переопределить не может.
jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
Re: Проблема хрупкого базового класса
От: Кодт Россия  
Дата: 06.11.15 17:55
Оценка:
Здравствуйте, _hum_, Вы писали:

__>Получается, в базовом классе надо с крайней осторожностью использовать виртуальные функции "для своих нужд" (Странно, что об этом так мало говорят).


Мало говорят о том, что виртуальные функции — особенно, в C++, где нет чёткого деления на интерфейсы и реализации, — играют несколько ролей.
1) Интерфейс для внешних пользователей.
2) Интерфейс потомков для предка. (Паттерн "шаблонный метод")

И есть ещё обратная штука — не требующая виртуальных функций или CRTP:
3) Интерфейс предка для потомков. Т.е., то, что предок предоставляет потомкам как инфраструктуру и строительные кубики.

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

Надо сказать, что проблема реентера, т.е. вызова функций в неподходящее время в несогласованном состоянии (частный случай — проблема зацикленной рекурсии) — более широкая.
class Lala;
struct IAlice
{
  virtual void listen(Lala*) = 0;
};

class Lala
{
  void trulala(IAlice* visitor) { take(rattle); visitor->listen(this); give(rattle); }
  void tralala(IAlice* visitor) { take(rattle); give(rattle); }
private:
  Rattle rattle;
};

class EvilAlice : public IAlice
{
  void listen(Lala* lala) override { if(tru) lala->tralala(this); }
  // Алиса не должна была заставить Траляля петь свою песенку, не дослушав Труляля...
  // но у неё сегодня было слишком плохое настроение.
  // В итоге погремушку испортили.
};

void tale()
{
  Lala lala;
  EvilAlice alice;
  lala->trulala(&alice);
}

В данном случае это, отчасти, решается разделением интерфейсов: Lala может реализовать интерфейс ILalaWithTakenRattle, или IRattleIndependentLala, или IRattleAwareLala — и отдавать Алисе исключительно его.
Но в ситуации повышенной доступности и доверчивости (а предок открыт и доверчив к потомкам) так, конечно, не сделать.
http://files.rsdn.org/4783/catsmiley.gif Перекуём баги на фичи!
Отредактировано 06.11.2015 21:57 Кодт . Предыдущая версия .
Re[6]: Проблема хрупкого базового класса
От: _hum_ Беларусь  
Дата: 06.11.15 18:33
Оценка:
Здравствуйте, jazzer, Вы писали:

J>Здравствуйте, _hum_, Вы писали:


__>>речь шла о том, что не удается придумать свод правил, рутинное выполнение которых гарантировало бы отсутствие эффекта хрупкого базового класса.


J>Я только что их привел. Или они не достаточно рутинные?


синонимы "рутинный": "нетворческий, однообразный, шаблонный". я пока не увидел правил, которые позволяют, не задумываясь, автоматом их выполнить и тем самым избежать FBC-проблемы...

J>>>API всегда надо продумывать, API это системы, компонента, или класса.


J>>>Я виртуальные вызовы визуализирую для себя как посылку сообщений по сети в распределенной системе, на которые другая сторона как-то реагирует и что-то присылает в ответ (в каком-то смысле, это отвечает изначальной формулировке ООП, когда объекты обмениваются сообщениями — просто надо это распространить и на общение базового и производного класса).


J>>>Так что, очевидно, ты не можешь вносить изменения в сервер, которые меняют протокол его общения с другим сервером, и ожидать, что ничего не сломается.


__>>извиняюсь, что вы понимаете под "протоколом". вот давайте, для конкретного примера:

__>>что здесь, по-вашему, относится к протоколу (общения между Set и CountingSet), который "не должен меняться"?

J>в этом примере протокол очень простой:

J>1) вызов add приводит к посылке производному классу сообщения add_impl
J>2) вызов addAll приводит к посылке производному классу сообщения addAll_impl

J>А изменение строчки (1) приводит к изменению второго пункта:

J>2) вызов addAll приводит к посылке производному классу N сообщений add_impl и затем сообщения addAll_impl.

J>Это именно изменение протокола. Если думать о взаимодействии базового и производного класса в терминах протокола, то все становится очевидно.

J>И NVI этот протокол четко специфицирует — за счет того, что нет виртуальных входов, вход только через NVI.

поскольку это пример NVI, я так понял, то, что вы написали, это и есть то, что вы подразумевали под "специфицированный протокол"? если да, то допускается ли несколько вызовов "impl-ов", и если допускается, описываются ли в протоколе ограничения на порядок их вызовов?

__>>эка лихо вы меняете позицию с признание проблемы в первом своем сообщении и низведение ее до надуманной теперь, когда выяснилось, что ваша точка зрения на ее решенность оказалась ошибочной.


J>Проблема решается в терминах протокола. NVI этот протокол прибивает гвоздями — потому что есть только сценарии, заданные NVI, их никто переопределить не может.


правильно ли я понимаю, проблема решается так:
интерфейс делают невиртуальным и прописывают, какие "обратные вызовы" (вызовы виртуальных функций) должны происходить при вызове того или иного метода интерфейса. это называют протоколом, и придерживаются принципа неизменности протокола в дальнейших модификациях базового класса, что гарантирует отсутствие эффекта FBC?
Re[2]: Проблема хрупкого базового класса
От: _hum_ Беларусь  
Дата: 06.11.15 18:52
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Здравствуйте, _hum_, Вы писали:


__>>Получается, в базовом классе надо с крайней осторожностью использовать виртуальные функции "для своих нужд" (Странно, что об этом так мало говорят).


К>Мало говорят о том, что виртуальные функции — особенно, в C++, где нет чёткого деления на интерфейсы и реализации, — играют несколько ролей.

К>1) Интерфейс для внешних пользователей.
К>2) Интерфейс потомков для предка. (Паттерн "шаблонный метод")

К>И есть ещё обратная штука — не требующая виртуальных функций или CRTP:

К>3) Интерфейс предка для потомков. Т.е., то, что предок предоставляет потомкам как инфраструктуру и строительные кубики.


Интересный взгляд. Возьму на заметку. Спасибо.


К>В данном примере функция inc1() оказалась и в публичном интерфейсе, и почему-то в инфраструктурном. (Собственно, чтобы убрать её из инфраструктурного — это нужно приложить какие-то волшебные силы, нагородить кучу изолирующего кода).

К>А это по дизайну неправильно.

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


так а общее решение проблемы-то какое?
выше jazzer, если я его правильно понял, предложил вариант — "NVI + фиксированный "протокол ответа предка потомку". как вы на это смотрите?
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.