Раньше считал, что если все непубличные члены класса имеют тип доступа 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();
}
//-------------------
};
//////////////////////////
Получается, в базовом классе надо с крайней осторожностью использовать виртуальные функции "для своих нужд" (Странно, что об этом так мало говорят).
Здравствуйте, 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.
Здравствуйте, _hum_, Вы писали:
__>Получается, в базовом классе надо с крайней осторожностью использовать виртуальные функции "для своих нужд" (Странно, что об этом так мало говорят).
Автор, дело в способе использования виртуальных методов. Если ты хочешь чтобы какой-то функционал мог быть перекрыт наследником то ты делаешь функцию виртуальной. Но тебе не обязательно делать такую общую возможность. Это же определенный класс и у него определенная задача. Именно так и нужно воспринимать виртуальные функции — кастомизация поведения в наследнике, а не полностью изменение функционала на 180 градусов.
Здравствуйте, _hum_, Вы писали:
__>Получается, в базовом классе надо с крайней осторожностью использовать виртуальные функции "для своих нужд" (Странно, что об этом так мало говорят).
Конечно же, говорят, и решение давно известно, просто ты не там ищешь. Гугли NVI
Здравствуйте, _hum_, Вы писали:
__>Здравствуйте, T4r4sB, Вы писали:
TB>>Это проблема незакрытой рекурсии
__>дык, механизм виртуальных функций и есть реализация в с++ незакрытой рекурсии. разве нет?
Здравствуйте, 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.
Здравствуйте, 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.
потому ваше замечание о том, что это проблема незакрытой рекурсии выглядит тавтологией (по отношению к моему высказыванию, что это проблема виртуальных функций).
Здравствуйте, _Artem_, Вы писали:
_A_>Здравствуйте, _hum_, Вы писали:
__>>Получается, в базовом классе надо с крайней осторожностью использовать виртуальные функции "для своих нужд" (Странно, что об этом так мало говорят).
_A_>Автор, дело в способе использования виртуальных методов. Если ты хочешь чтобы какой-то функционал мог быть перекрыт наследником то ты делаешь функцию виртуальной. Но тебе не обязательно делать такую общую возможность. Это же определенный класс и у него определенная задача. Именно так и нужно воспринимать виртуальные функции — кастомизация поведения в наследнике, а не полностью изменение функционала на 180 градусов.
похоже на "у хорошего программиста и goto нормально работают"
Здравствуйте, _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.
Ну, вообще-то, да, программирование требует дисциплины — это новость для кого-то?
API всегда надо продумывать, API это системы, компонента, или класса.
Я виртуальные вызовы визуализирую для себя как посылку сообщений по сети в распределенной системе, на которые другая сторона как-то реагирует и что-то присылает в ответ (в каком-то смысле, это отвечает изначальной формулировке ООП, когда объекты обмениваются сообщениями — просто надо это распространить и на общение базового и производного класса).
Так что, очевидно, ты не можешь вносить изменения в сервер, которые меняют протокол его общения с другим сервером, и ожидать, что ничего не сломается.
Так что когда говоря, что типа безопасные изменения в базовом классе ломают наследников — это не безопасные изменения, если они меняют протокол общения.
Все примеры, которые тут приводились (в т.ч. по ссылкам) — все об изменении протокола. Нужно всего лишь думать об этом, когда вносишь изменения: меняешь ты этими изменениями протокол или нет.
Вот и все.
Ничего запредельного тут нет.
Для любого, кто писал распределенные приложения, это азы и этот анализ делается спинным мозгом.
Нужно просто начать думать об этом во время работы с иерархиями наследования тоже.
Здравствуйте, 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.
J>Ну, вообще-то, да, программирование требует дисциплины — это новость для кого-то?
речь шла о том, что не удается придумать свод правил, рутинное выполнение которых гарантировало бы отсутствие эффекта хрупкого базового класса.
J>API всегда надо продумывать, API это системы, компонента, или класса.
J>Я виртуальные вызовы визуализирую для себя как посылку сообщений по сети в распределенной системе, на которые другая сторона как-то реагирует и что-то присылает в ответ (в каком-то смысле, это отвечает изначальной формулировке ООП, когда объекты обмениваются сообщениями — просто надо это распространить и на общение базового и производного класса).
J>Так что, очевидно, ты не можешь вносить изменения в сервер, которые меняют протокол его общения с другим сервером, и ожидать, что ничего не сломается.
извиняюсь, что вы понимаете под "протоколом". вот давайте, для конкретного примера:
что здесь, по-вашему, относится к протоколу (общения между Set и CountingSet), который "не должен меняться"?
J>Так что когда говоря, что типа безопасные изменения в базовом классе ломают наследников — это не безопасные изменения, если они меняют протокол общения. J>Все примеры, которые тут приводились (в т.ч. по ссылкам) — все об изменении протокола. Нужно всего лишь думать об этом, когда вносишь изменения: меняешь ты этими изменениями протокол или нет. J>Вот и все. J>Ничего запредельного тут нет. J>Для любого, кто писал распределенные приложения, это азы и этот анализ делается спинным мозгом. J>Нужно просто начать думать об этом во время работы с иерархиями наследования тоже.
эка лихо вы меняете позицию с признание проблемы в первом своем сообщении и низведение ее до надуманной теперь, когда выяснилось, что ваша точка зрения на ее решенность оказалась ошибочной.
Здравствуйте, _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, их никто переопределить не может.
Здравствуйте, _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 — и отдавать Алисе исключительно его.
Но в ситуации повышенной доступности и доверчивости (а предок открыт и доверчив к потомкам) так, конечно, не сделать.
Здравствуйте, 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?
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, _hum_, Вы писали:
__>>Получается, в базовом классе надо с крайней осторожностью использовать виртуальные функции "для своих нужд" (Странно, что об этом так мало говорят).
К>Мало говорят о том, что виртуальные функции — особенно, в C++, где нет чёткого деления на интерфейсы и реализации, — играют несколько ролей. К>1) Интерфейс для внешних пользователей. К>2) Интерфейс потомков для предка. (Паттерн "шаблонный метод")
К>И есть ещё обратная штука — не требующая виртуальных функций или CRTP: К>3) Интерфейс предка для потомков. Т.е., то, что предок предоставляет потомкам как инфраструктуру и строительные кубики.
Интересный взгляд. Возьму на заметку. Спасибо.
К>В данном примере функция inc1() оказалась и в публичном интерфейсе, и почему-то в инфраструктурном. (Собственно, чтобы убрать её из инфраструктурного — это нужно приложить какие-то волшебные силы, нагородить кучу изолирующего кода). К>А это по дизайну неправильно.
К>Надо сказать, что проблема реентера, т.е. вызова функций в неподходящее время в несогласованном состоянии (частный случай — проблема зацикленной рекурсии) — более широкая.
так а общее решение проблемы-то какое?
выше jazzer, если я его правильно понял, предложил вариант — "NVI + фиксированный "протокол ответа предка потомку". как вы на это смотрите?