1. C++ / Говнокод #9790

    +163

    1. 1
    2. 2
    3. 3
    4. 4
    5. 5
    THTTPServer::TDynamicResponse::~TDynamicResponse( void )
    {
    	if(typeid(*this)==typeid(TDynamicResponse))//Борьба с pure virtual function call.
    		this->flush();
    };

    Проект поменьше.

    Запостил: Говногость, 28 Марта 2012

    Комментарии (62) RSS

    • интересно, после такого патча автор успокоился и выставил в багтрекере FIXED?
      Ответить
      • Тут в каждом наследнике некого класса TResponse стоит эта магическая комбинация строчек.
        Просто flush - виртуальная функция и если вызвать flush в деструкторе предка (а не в деструкторе реального объекта), то вызывается не тот flush, тк реальный объект к этому времени деструктирован.
        Автор написал такой воркэраунд вокруг этой проблемы.
        Я пока это не переписывал и пока не знаю как, но планирую.
        Ответить
        • если вызывать flush в деструкторе предка (например, base), то будет вызван именно base::flush()
          http://ideone.com/0jc2b
          воркэраунд ни о чем, это просто изначально идиотская идея пытаться в деструктор предка засунуть несвойственные ему задачи - якобы пытаться в нем выполнить виртуальные операции, за которые он никакой ответственности нести не имеет права, а потом с этим героически бороться
          Ответить
          • >если вызывать flush в деструкторе предка (например, base), то будет вызван именно base::flush()
            Капитанские погоны вам к лицу.

            >воркэраунд ни о чем
            Как не о чем? Свою задачу выполняет. Вызывается именно flush реального типа объекта, а не base. Добиваются этого именно копипастой этих магических строк и заменой TDynamicResponse на имя деcтруктора без символа ~.

            Если знаете как это исправить правильно, то говорите, не стесняйтесь.
            Ответить
            • > тк реальный объект к этому времени деструктирован
              >> //Борьба с pure virtual function call
              > Капитанские погоны вам к лицу.
              не к лицу, просто неясность изъявления ваших мыслей привела меня к выводу, что работа деструкторов для кого то хранит слишком много тайн

              если идея в том, что каждый предок имеет свой собственный flush, который своим вызовом должен исключить дальнейший вызов flush предка в деструкторе, то гораздо проще этого добиться с помощью protected: bool was_flushed_ в базовом классе и вызовом с банальной проверкой if (!was_flushed_) { this->flush(); was_flushed_ = true; }

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

              > Свою задачу выполняет
              каким образом он может выполнить свою задачу, если в деструкторе объекта ~sometype() равенство typeid(*this) == typeid(sometype) - это тождество?
              http://ideone.com/cME4z
              Ответить
              • >каким образом он может выполнить свою задачу, если в деструкторе объекта ~sometype() равенство typeid(*this) == typeid(sometype) - это тождество?
                http://ideone.com/cME4z
                А вот это уже интересно... В студии выполняется, а в GCC нет... Кто прав? GCC или студия 2008?
                Ответить
                • То есть в студии 2008 вылезет только одна единица. Та, что с derived.
                  Ответить
                • в моей 2008 студии всё то же самое
                  1
                  1
                  Для продолжения нажмите любую клавишу . . .

                  Microsoft Visual Studio 2008
                  Версия 9.0.30729.1 SP

                  но если в другом компиляторе будет иное поведение, то это лишний раз доказывает некорректность притягивания RTTI к решению проблемы в деструкторах
                  Ответить
                  • >в моей 2008 студии всё то же самое
                    OMG, по непонятной причине работает - не буду трогать... :-[
                    Ответить
                    • > OMG, по непонятной причине работает
                      Тарас, заметь: даже программисты на цпп со стажем не понимают, почему оно работает. Вот уж действительно КРЕСТОПРОБЛЕМЫ™. Хотя я недавно собеседовал Java-программиста с 6-летним стажем, не знающего элементарных вещей.
                      Ответить
                      • Ну там рахитектура реально говно и я не особо пытался разбираться. Поэтому и не знаю почему оно работает. Может и правда несколько раз flush вызывает. Я это менять не должен, тк у меня другая задача, просто случайно в этот чужой исходник заглянул, стало весело и одновременно стремно.
                        Ответить
                      • ну тут явное забивание шурупов перфоратором
                        RTTI вообще сам по себе тот еще костыль, и очень вендор-специфик
                        запросто ожидаются даже проблемы с нерабочим dynamic_cast<>, если библиотеку и приложение собрать разными версиями одного компилятора
                        да и в конструкторах и деструкторах RTTI противопоказан, очень неудачная идея его там использовать
                        Ответить
                        • >запросто ожидаются даже проблемы с нерабочим dynamic_cast<>, если библиотеку и приложение собрать разными версиями одного компилятора
                          Меня это тоже раздражает... Неужели стандартизаторы С++ до сих пор не смогли стандартизировать ABI для RTTI? Казалось бы dynamic_cast - конструкция языка, но вдруг неожиданного может перестать работать, если взять более новую версию библиотеки.
                          Ответить
                          • стоит перестать злоупотреблять dynamic_cast, и тогда и седативные не понадобятся
                            это всего лишь костыль, призванный в сжатые сроки ставить подпорки в разваливающуюся глиняную рахитектуру приложения, поэтому в хорошем проекте grep -r "dynamic_cast" обязан выдать 0 результатов даже на миллионе LoC
                            Ответить
                            • А задачи, требующие паттерна мультиметод вы как разруливаете без dynamic_cast? Тут кастить в любом случае придётся. С++ встроено мультиметод не поддерживает.
                              Ответить
                              • > С++ ... мультиметод не поддерживает
                                Use Lisp, Luke.
                                Ответить
                                • Ну не всегда это возможно. Например при программировании каких-нибудь микроконтролёров и систем реального времени.
                                  Ответить
                              • всё что угодно можно разрулить без dynamic_cast
                                virtual integraltype get_type() const;
                                void do_multi(base * b1, base * b2) и дальше уже как фантазия разыграется делать соответствие make_pair(b1->get_type(), b2->get_type()) и методу
                                Ответить
                                • А за уникальностью значения get_type кто следить будет? Погромист? Место для труднообнаружимых ошибок.
                                  Ответить
                                  • в этом есть как плюсы так и минусы - например, 1) гораздо выше скорость 2) можно будет группировать классы
                                    в любом случае С++ мультиметод не будет таким красивым, как ему положено быть в приспособленных для этого языках
                                    Ответить
                                    • >2) можно будет группировать классы
                                      Можно пример?
                                      Ответить
                                      • если два класса в иерархии заявят что они type_eatable, это позволит их единообразно обработать
                                        возможно будет выглядеть красивее, чем
                                        my::some::inner::classtype1 * c1;
                                        my::some::other::classtype2 * c2;
                                        my::some::inner::classtype3 * c3;
                                        my::some::other::classtype4 * c4;
                                        if ((c1 = dynamic_cast<my::some::inner::classtype1 *>(b1))
                                        		&& ((c2 = dynamic_cast<my::some::other::classtype2 *>(b2)))
                                        	do_as_c1_c2(c1, c2);
                                        else if (((c3 = dynamic_cast<my::some::inner::classtype3 *>(b1)) 
                                        		&& ((c4 = dynamic_cast<my::some::other::classtype4 *>(b2)))
                                        	do_as_c3_c4(c3, c4);


                                        впрочем, я не знаю как там у вас делаются мультиметоды, потому что у меня их нет вовсе
                                        Ответить
                                        • >если два класса в иерархии заявят что они type_eatable, это позволит их единообразно обработать
                                          Тут вся сложность в том, что несколько классов могут по ошибке заявить, что они type_eatable и поэтому они не верно будут обрабатываться, что возможно заметят далеко не сразу.
                                          Ответить
                      • Чёрт, как же происходит в Аде, я даже не знаю...
                        Что-то я начинаю думать (извините, но я реально так начинаю думать), что РАИИ и ООП несовместимы. РАИИ оставьте для всякой хрени, которая маскируется под встроенный тип, а ООП с РТТИ, наследованиями и всяким динамоговном лучше оставить ссылочным и без всякой неявной херни вообще, т.е. с ручный конструктором и деструктором.
                        Можно заворачивать говнообъект в РАИИ-указатель, можно подключать ГЦ, действующий только для таких объектов.
                        Ответить
                        • В жабе с её рефлексией всё гораздо проще и довольно сносно сочетается с ООП (кроме него в жабе ничего больше и нет...).
                          Ответить
                          • Да, в жабу бы добавить тупые структуры (не ссылочные) с РАИИ, будет та модель, которую я говорю. Но так как вся стандартная библа заточена под гэцэблядство, но в жабу вводить это поздно.
                            Ответить
                        • >ООП с РТТИ, наследованиями и всяким динамоговном лучше оставить ссылочным и без всякой неявной херни вообще, т.е. с ручный конструктором и деструктором.
                          А какая разница ссылочные типы или не ссылочные? Будет та же проблема, что видна в этом топике. А уж тем более без разницы ручные деструкторы или автоматические.
                          Ответить
                          • Уберётся неявный вызов деструктора родителя.
                            Вызываться будет только то, что вызвалось.
                            Ответить
                            • Ну так это не ручные деструкторы, а полуавтоматические. Некоторые считают, что так и должно быть. То есть деструкторы ручные только внутри класса, а вот снаружи объекта они автоматические.
                              Ответить
                              • Никакой автоматики для ООПни.
                                Желающие пусть сами заводят структуру, содержащую класс и вызывающую виртуальный метод flush в деструкторе этой структуры. И flush вызовется лишь 1 раз.
                                Ответить
                      • > Java-программиста с 6-летним стажем, не знающего элементарных вещей.
                        ммм, это каких же?
                        Ответить
                • Это в какой версии GCC оно не выполняется?
                  Ответить
              • > Свою задачу выполняет
                каким образом он может выполнить свою задачу, если в деструкторе объекта ~sometype() равенство typeid(*this) == typeid(sometype) - это тождество?
                Я понимаю о чем вы, просто автор кода видимо думал, что в деструкторе\конструкторе typeid работает также, как и везде.
                В С++ это не так.
                А вот стандартное поведение:
                http://ideone.com/lNoNx
                Видимо, автор думал, что код в деструкторе работает также.
                Я понимаю что работать этот код не должен, но почему то работает. Думаю, там ещё какие то проверки и флаги есть, которые всё-таки работают. Я в тот исходник смотрел только 2 минуты, но больше смотреть не хочу. Не моё это дело переписывать чужой работающий говнокод.
                Ответить
            • Неверно.

              На территории конструктора и деструктора класса 'A' (т.е. непосредственно внутри реализации 'A::A' и 'A::~A') выражение 'typeid(*this)' всегда возвращает строго 'typeid(A)', незавивисимо от того, какой тип имеет "полный" конструируемый или уничтожаемый объект.

              По этой причине в данном примере условие под 'if' - тавтология. Оно всегда истинно. Поэтому соврешенно не понятно, во-первых, что пытались сделать, и, во-вторых, чего достигли. Данный 'if' - бессмысленен и ничего не меняет.

              Никакой борьбы с pure virtual function call таким способом получить не удастся.
              Ответить
              • Мне как человеку постороннему может быть позволено спросить: а возможна ли такая ситуация, что класс наследник в деструкторе обнулил this, а потом вызвал деструктор суперкласса?
                Ответить
                • Что имеется в виду под "обнулил this"? В стандартном С++ 'this' не является lvalue. Его невозможно ни обнулить, ни как либо еще модифицировать. Также к нему, например, невозможно применить оператор взятия адреса '&'. Т.е. 'this' - это некое эфемерное указательное значение, которое с точки зрения языка нигде физически не хранится. Его можно использовать только "по чтению".
                  Ответить
                  • Я имел в виду delete this. На что после этого будет указывать *this?
                    Ответить
        • Ну как, переписал?
          Ответить
      • Теперь каждый раз создавая новый потомок TResponse, пусть и далёкий, но приходиться копипастить эти магические строчки. Притом иной раз и не заметишь, как забыл скопипастить. Говноархитектуры не нужны.
        Ответить
    • няшаблядская привычка писать void в скобках
      Ответить
    • Я правильно понял, что более одного уровня наследования здесь не предполагается?
      Ответить
      • Нет, там штук 7 уровней, притом там всего 7 классов в этой иерархии и они все используются.
        Ответить
        • Т.е. деструкторы последнего будут вызывать flush семь раз подряд? Надёжненько.
          Ответить
          • У меня есть подозрения, что автор кода планировал используя
            if(typeid(*this)==typeid(TDynamicRespons e))
            избежать 7ми кратного вызова flush
            Ответить
            • Это-то понятно, но выше уже указывалось, что бессмысленно. Вот уж борьба так борьба.
              Ответить
    • Я придумал. Чтобы flush вызывался ровно один раз, надо вызывать его только в деструкторе базового класса.
      Чтобы он соответствовал typetag, надо делать его виртуальным.
      Ответить
      • Я подозреваю, что в flush флажок какой-то у нас проверяется. Мол уже выполнен или ещё нет. А так вызывается 7 раз. А может и не проверяется, тогда не знаю как он работает, повезло.

        В деструкторе базового класса в С++ вызывать flush нельзя, тк класс потомок уже деструктирован и вызов будет некорректный. Вызовется flush базового класса или pure virtual call method stub, если метод flush базового класса не имеет реализации.
        Ответить
        • > В деструкторе базового класса в С++ вызывать flush нельзя, тк класс потомок уже деструктирован и вызов будет некорректный.

          Чё? Все деструкторы пустые, кроме базового, они ниего не будут делать, деструктор базового вызовется как раз вовремя.
          Ответить
          • Если flush в базовом классе pure virtual, то его вызов из деструктора базового класса - это 100% крэш с pure virtual function call.

            Вообще, любая попытка вызвать любую виртуальную функцию из конструктора или деструктора - говнокод по определению. Это же крестоосновы.
            Ответить
            • А нет средств вызывать функции из деструктора не прямо, а через ТВМ?
              Ответить
              • через ТВМ будет вендорозависимо
                но всегда можно выстрелить себе в ногу и без помощи ТВМ - сохранить в конструкторе derived адрес метода в поле base::my_virtual_flush_ (типа = boost::bind(derived::flush, this)) и его вызывать в base::~base()

                только это всё равно будет мегаговнокостыль
                потому что derived::flush() наверняка захочет поработать с какими-нибудь членами derived, а они уже давно разрушены, потому то из коробки виртуальные функции в конструкторах и деструкторах в С++ работают как работают
                Ответить
                • Я вспомнил, да, та же проблема с конструкторами.
                  В общем, сдаётся мне, что наследование и РАИИ несовместимы
                  Ответить
                • Вызовы виратульных методов через указатель-на-метод все равно идут через ТВМ (именно в момент вызова). Поэтому при использовании обычных языковых указателей-на-метод ничего не поменяется: несмотря на то, что в 'base::my_virtual_flush_' был запомнен указатель якобы на 'derived::flush', при вызове изнутри 'base::~base' все рано будет вызван 'base::flush'.

                  А 'boost'... Неужели 'boost' предоставляет возможности для того, чтобы подавить это поведение, т.е. привязать указатель непосредственно к конкретной виртуальной функции еще на этапе инициализации из исключить ТВМ из рассмотрения в момент вызова?
                  Ответить
                  • > виратульных

                    ЗА ВИРАТУЛА!!!

                    Ахаха я знаю как назову следующего виртуала
                    Ответить
                  • ты прав
                    биндить виртуальную функцию - попадать в ТВМ
                    ну значит решение выстреливания себе в ногу - для каждой виртуальной inherited::flush() нужна невиртуальная inherited::do_flush(), делающая то, что нужно
                    в невиртуальной do_flush(), понятное дело, нельзя другие виртуальные вызовы

                    http://ideone.com/hm5Ve
                    Ответить
              • Со стороны реализации вирутальные функции в конструкторе/деструкторе ведут себя так именно потому, что к этом моменту указатель ТВМ объекта уже указывает на ТВМ конструируемого/деструктируемого класса. Поэтому попытка залезть в текущую ТВМ абсолютно ничего не изменит.
                Ответить
            • Неверно.

              Виртуальность в конструкторе/деструкторе прокрасно работает, но лишь до уровня конструируемого/деструктируемого класса. Логика этого поведения - общеизвестна.

              Помня об этом, можно спокойно вызывать виртуальные функции из конструктора/деструктора.
              Ответить
              • А что такое эта "виртуальность до уровня"? Это то же самое, что и вызов обычного (невиртуального) метода. Потому что о любых его версиях в базовых классах компилятор и так знает. А виртуальными функции делаются (и вызываются) как раз для того, чтобы расширить базовое поведение каким-то новым, в общем случае неизвестным. В конструкторе/деструкторе это невозможно.

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

                А высший пилотаж - это когда конструктор или деструктор вызывает обычную функцию, та - ещё десяток обычных, а какая-то из них на десятом уровне вложенности добирается до виртуальной... это уже никакой компилятор сам не найдёт :(
                Ответить
                • "То же самое что и вызов обычного (невиртуального) метода", получается только в том случае, когда мы говорим о вызове какой-то виртуальной функции напрямую из конструктора.

                  Но как только мы начинаем рассматривать "опосредованные" вызовы, ситуация меняется. Виртуальные механизмы во время конструкции/деструкции продолжают работать так же, как и работали. Меняется только динамический тип объекта. А виртуальность работает по-старому в рамках "нового" динамического типа.

                  Например, пусть у нас есть три класса

                  struct A { 
                     virtual void foo() {}
                     void bar() { foo(); }
                  };
                  
                  struct B : A {
                     virtual void foo() {}
                     ~B() { bar(); }
                  };
                  
                  struct C : B {
                     virtual void foo() {}
                  };
                  
                  int main() {
                     C c;
                     c.bar(); // вызывает 'A::bar', которая вызывает 'C::foo'
                     // во время деструкции 'c' вызовется  'A::bar', которая вызывает 'B::foo'
                  }


                  В данном примере вызов 'foo' внутри 'A::bar' выполнятеся виртуально. Но при вызове 'A::bar' из деструктора 'B::~B' виртуальность ограничена глубиной класса 'B'.
                  Ответить
                  • Да, она данном случае работает, вопрос только один: зачем такое делать? Если B хочется из деструктора вызвать свою версию foo() - он может это сделать напрямую (независимо от её виртуальности), не выпендриваясь с опосредованностью. Если bar() - это какой-нибудь сложный алгоритм, работающий с чем-то, инкапсулированным в A и вызывающий виртуальные функции при особых условиях, то запускать что-то подобное на уже полуразрушенном и нестабильном объекте, мягко говоря, странно. Ведь тот, кто писал этот алгоритм, вполне мог расчитывать на то, что foo() будет вызываться именно из того объекта, который изначально конструировался. От этого может (и часто - будет) напрямую зависеть эффективность и безопасность такого вызова.

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

                    В C++ вообще много чего можно делать, но не всё - нужно.
                    Ответить
    • Дело в том, что когда удаляется деструктор наследуемого класса, сначала вызовется деструктор наследника, а потом - деструктор предка, причем в момент вызова деструктора предка класс уже в него кастанулся и все виртуальные методы также будут методами предка.
      Варианта два: либо выносить всю логику flush() в предка, либо вызывать flush() в дектрукторе каждого наследника.
      Ответить

    Добавить комментарий