Pull to refresh
57
70.5

Пользователь

Send message

Правило же должно звучать не как «не вызывать виртуальные функции», а «вызывая виртуальные функции, не мечтать о вызове непонятно чего, а отдавать себе отчёт, что может быть вызвано, а что нет».

Можете сами попробовать. Только для чистоты эксперимента без фреймворков, голый WinAPI.
Я и пишу на голом WinAPI на C/C++ без всяких фреймворков.
То, что вы описываете, для меня выглядит как нонсенс. Не наблюдал я такого в десятках и сотнях случаях, когда подобные циклы возникали.

Sleep(1) «помог бы» (по вашим словам), но Sleep(1) сделает так, что то, что мега-фукнция вычислила бы за секунду, она будет вычислять за полторы минуты.


Единственное вразумительное объяснение, которое я могу дать вашим наблюдением вот такое: у вас на машине, где вы это наблюдали, была катастрофическая ситуация с системой охлаждения. Высохшая/неправильно нанесённая термопаста, например, или что-то в таком духе.


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


В норме того, что вы описываете, не происходит.

Речь шла совершенно о другом.
Недавно тут была статья «Как понять, чо вы пишите не на C++, а на Си с классами».


Так вот, если вы пореверсите продукты MS, написанные на C++, да хоть тот же обсуждаемый здесь VB6, то откроете для себя, что внутри MS для своих же продуктов С++ используется как «Си с классами».


Они не используют STL или сишный механизм исключений внутри своих продуктов. Не на уровне API ОС, не между разными DLL-библиотеками, а внутри одной программы/библиотеки они это обходят стороной.

Банальные утечки памяти или незакрытые handle. Один из сценариев когда goto мог создать проблемы несмотря на безопасную среду.

Ну так и файл, открытый родными средствами средствами VB, не будет закрыт при выходе из процедеры. И оконо, показанное из процедуры, тоже не будет закрыто.


А если обернуть в класс хоть получение хендла через WinAPI, хоть открытие файла родными средствами — тогда экземпляр класса будет правильным образом уничтожен (если других ссылок нет).


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


Ну и к этому относится и использование "хаков" вроде прямых вызовов управляющих функций вирт. машины чтобы что-то там оптимизировать, все это один и тот же сценарий проблем с goto по сути.

Что за управляющие вызов виртуальной машины? Имплементации P-кодных инструкций не экспортируются — вызвать их нельзя. Да и функциями эти имплементации не являются.


Из управляющих можно назвать разве что ProcCallEngline/ProcMethEngline — эдакие гейты между native-кодом и P-кодом — позволяют вызывать P-код из native-среды.


Но не представляю, каким образом их использование совместно с goto может нести какие-то проблемы.

Кто говорил о кросс-процессности?

"нтерпретация байт-кода" для режима P-CODE, потому что в таком режиме библиотека машины все равно интерпретирует его по ходу выполнения


Тогда скомпилированные Java-программы работают путём интерпретации Java-байкода, дотнетовские программы работают путём интерпретации MSIL-байткода. Да и процессоры интерпретируют машинный код.


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

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


Но она же не так делает. В том большом комменте описано, как она это делает.
Коротко и упрощённо говоря: для каждой P-кодной инструкции есть свой хендлер, опкод P-кодной инструкции является индексом в таблице указателей на соответствующие обработчики.


Приведённое выше академическое определение понятия «интерпретор» настаивает на том, что входными данными для него является исходный текст программы, а обработка происходит построчно.


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


В теории можно было бы вообще создать архитектуру процессора, которая бы машинно умела исполнять VB-шный P-код. Ну какая же это интерпретация?

Дорого создавать его в каждой функции, где нужна хоть какая-то обработка, добавлять в список и исключать из списка по выходу.

Да что же в этом дорогого? Это не было дорогим удовольствием 25 лет назад, а уж тем более сейчас.


Это не дороже, чем две лишних локальных переменных.

Я смотрю, пока я писал, вы перекроили свой коммент.


или например нативные вызовы winapi которые после перехода останутся незакрытыми.

Как понять «незакрытый вызов WinAPI»? Вызовы WinAPI были не хаком, а штатной возможностью, точнее даже были осуществимы через два разных механизма: через Declare Sub/Function и через TLB.


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

Процедуры генерировались в чистейший машинный код.


Давайте разделять понятия: виртуальная машина — это то, что, принимая на вход цепочку команд, каким-то образом выполняет их, предпринимая какие-то действия по поводу каждой команды и меняя своё внутреннее состояние.


Реализация виртуальной машины VB-шного P-кода жила в MSVBVMxx.DLL (что и давало название этой библиотеки), но помимо виртуальной машины значительную часть библиотеки составляло то, что можно назвать «стандартной библиотекой» по аналогии с языками Си/Си++. Т.е. это просто различные «встроенные функции», заявленные как «часть языка», а также служебные функции (в случае C++ примером такой функции может стать _purecall). Это обычные процедуры в виде машинного кода, они к виртуальной машине отношения не имеют.


Так вот, то, что генерировалось в режиме создания Native-кодных исполняемых файлов, представляло собой обычный машинный код x86 и не с вкраплениями P-кодных кусочков, а с вкраплениями вызовов служебны функций для выполнения тех задач, которые приходится очень часто решать и которые раздули бы объём кода, если бы эти задачи инлайнились.


Например — копирование структур. Копирование структур можно было бы «инлайнить», сгенерировав код для копирования каждого члена структуры (в общем случае структуру нельзя копировать банальными memcpy, ведь там могут быть указатели на строки или COM-интерфейсы, массивы — и копировать это надо по умному). Вместо генерировалась одна единственная call-инструкция, вызывающая функцию рантайма, которая брала на себя все заботы по правильному копированию структуры. Ей передавался некий дескриптор структуры, предопределявший правила копирования.


Или же после каждого вызова метода COM-интерфейса (читай «метода вызова любого VB-объекта или любого внешнего COM-объекта»), который мог выбросить ошибку путём возврата HRESULT, ставился вызов __vbaHresultCheck, который брал на себя всю работу по «транслированию» кода сбоя (HRESULT) в VB-ошибку (VB использовал SEH-исключения для работы своего механизма ошибок).

Ну а в том комментарии кстати кажется упущен еще 1 момент для полноты, а именно опция сбора итоговых ехешников не только в байткод (который был медленнее + на раз-два декомпилировался без защиты), но и в натив со своими особенностями.

Плохо читали — ничего там не упущено. Оно скрыто под спойлером «Немного подробностей о том, как происходит компиляция в Native-код», внутри которого есть свой под-спойлер «Интересный вопрос касательно IL на входе бэкенда C2».


Ну и поскольку она при работе интерпретирует байткод (который так вот круто сгенерирован IDE еще в процессе написания), то это все же интерпретатор.

В Википедии приводится вот такое определения термина «интерпретация» в контексте ЯП:
Интерпрета́ция — построчный анализ, обработка и выполнение исходного кода программы или запроса, в отличие от компиляции, где весь текст программы, перед запуском анализируется и транслируется в машинный или байт-код без её выполнения[4][5][6]


Источниками такого определения назван не Вася Пупкин, а литература, написанная авторами, заслуживающими уважения:


Источники
  • Першиков В. И., Савинков В. М. Толковый словарь по информатике / Рецензенты: канд. физ.-мат. наук А. С. Марков и д-р физ.-мат. наук И. В. Поттосин. — М.: Финансы и статистика, 1991. — 543 с. — 50 000 экз. — ISBN 5-279-00367-0.
  • Борковский А. Б. Англо-русский словарь по программированию и информатике (с толкованиями). — М.: Русский язык, 1990. — 335 с. — 50 050 (доп.) экз. — ISBN 5-200-01169-3.
  • Толковый словарь по вычислительным системам = Dictionary of Computing / Под ред. В. Иллингуорта и др.: Пер. с англ. А. К. Белоцкого и др.; Под ред. Е. К. Масловского. — М.: Машиностроение, 1990. — 560 с. — 70 000 (доп.) экз. — ISBN 5-217-00617-X (СССР), ISBN 0-19-853913-4 (Великобритания).

Так что в соответствтии с общеприянтым определением это не интерпретация, а компиляция и исполнение байт-кода виртуальной машины. Видимо многим просто нелегко принять факт, что презираемый очень многими инструмент с репутацией «для чайников в программирование» имел крутейший и непревзойдённый механизм патчинга скомпилированного байт-кода по живому. «Ну не может же такого быть, что оно вот так легко на лету перекомпилирует и перекраивает скомпилированную процедуру, в данный момент исполнявшуюся — значит там под капотом банальная интерпретация» — так видимо рассуждало большинство.

Windows: Structured Exception Handling. Очень вкусно именно в этом плане, но
1) Дороговато

SEH-фрейм занимает 8 байт 2 sizeof(void) на стеке — разве это дорого? В обходе односвязного списка и вызове обработчика по указанному адресу тоже вряд ли что-то дорогое можно узреть. Скорее всего по сравнению с тем, что обработчики будут делать, overhead от обхода цепочки вызовов будет незначительным.


SEH был дорогим по совсем другой причине. Вызов RaiseException приводит к переходу в режим ядра, а это довольно медлительная вещь. В этом смысле SEH-исключения не могли стать альтернативой для кодов возрата статуса и должны были использоваться только для реально исключительны ситуаций. Попытка переделать цикл, проходящийся по миллиону элементов и вызывающий для каждого элемента какую-нибудь функцию, которая с вероятностью 30...50% может сбойнуть на выкидывания исключений очень значительно замедлила бы работу такого цикла, потому что 500 000 раз выкидывать исключение — это 500 переходов в режим ядра и обратно, что на порядок или несколько порядков медленнее, чем если бы вызываемая функция просто возвращала код ошибки, при условии что в в вызываемой функции только какая-нибудь арифметика и манипуляция данными и нет системных вызовов.

Как это элегантно сформулировать?


«В Си в стиле WinAPI или POSIX или подавляющего большинства паттернов обработки ошибок в Си или С++ в стилке Microsoft»?


Что характерно, ядро Windows написано на Си, но там очень много где (если IRQL позволяет) используется расширение try...except для обуздания SEH.

Почему не в «Си или C++ в стиле Microsoft»?


При вызове WinAPI вы обязаны проверять GetLastError cами.
При вызове COM-методов вы обязаны проверять HRESULT сами.

А в VB иначе работало все же, там интерпретатор позволял выходить так из любой (или почти любой) задницы "вполне законно".

Я вас умоляю. Читайте вот этот коммент до просветления:
https://habr.com/ru/post/582566/#comment_23578554

но могут всю систему притормозить (такая уж многозадачность в винде).

В Windows 9x разве что. Хорошая многозадачность в винде, не надо наговаривать. По крайней мере на уровне ядра всё сделано безупречно.


Вот пользовательская подсистема может грешить. Один из примеров грешка: буфер обмена. Если какое-то приложение захватит буфер обмена и не будет отпускать, другие приложения, которые тоже захотят получить доступ к буферу обмена, встанут. Но те, которые не попытаются, не встанут уж точно.


Не согласны — предоставьте PoC-код подвешивания системы длительным глухим циклом.

Разумный подход, как я считаю, состоит в том, чтобы не использовать GoTo, чтобы на его базе делать циклы. Не надо использовать GoTo вместо For, вместо While, вместо Do. Не надо использовать GoTo для перепрыгивания блока кода вместо использования блочного If или If Else.


Но вообще объявлять GoTo абсолютным злом и запрещать его использовать абсолютно всегда — это глупость и максимализм.


Тогда нужно запретить и break/continue в Си, ведь это тот же goto.


Более того, Джоэль Спольски как-то высказал мысль, что обработка исключений — это тот же скрытый GoTo, только даже не в пределах одной процедуры, а через несколько процедур.

Если вы используете контрол и не соблюдаете контракты на его параметры — исправьте вызовы, если при соблюдении контрактов контрол ведет себя некорректно — нужно чинить контрол,

Такие контракты могут просто отсутствовать, именно так и было для большинства контролов во времена «золотого времени» VB. Если вы пишите контрол на том же VB, вы обычно вообще не имплементируете свойства Height/Width, имплементация как бы унаследована от базового класса UserControl.


Но бог с ними, с контрактами.


Перемасштабирование не удалось потому, что не хватило памяти. Что более умное можно сделать, кроме как игнорировать эту ошибку?


Если вообще не обрабатывать её, программа вылетит.
Если обрабатывать, то что делать?


Ничего не делать — тот же On Error Resume Next.
Выходить из процедуры — ломать ресайз всего остального UI из-за одного дефективного контрола.
Выводить сообщение об ошибке — бредовейшая затея. Ресайз превращается в ад (смещение на каждый пиксель вызывает новое сообщение), а в условиях жесткой нехватки памяти попытка показать сообщение обернётся новым исключением/ошибкой, которую, как правило, никто не уже не будет ловить, а это вылет приложения.


Вы подходите с позиции идеалистов, которые считают, что нужно всё покрыть тестами, что есть классный отдел Q&A, который все дефекты выявит, и пользователь получит классный продукт.


Это недостижимая утопия. Те же ОС тестируют в хвост и в гриву, а критические уязвимости и ошибки всё равно появляются.


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


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

Есть IIf, что приближённо соответствует тернарному ?: в Си. С этой проверкой, как я уже написал, вы застрахуете себя от отрицательных координат, но если автору контрола вздумается, что минимальным размером контрола должно быть 40×80, а не 0×0, ваша страховка внезапно потеряет силу. Но даже если не вздумается, вы не застрахуете себя от Out of memory или Out of stack space. Реально невозможность подвинуть контрол должна быть поводом аварийного завершения программы и потери всех данных? Кроме того, реализация контрола всё равно будет проверять входные данные.


и ошибка в самой попытке присваивания,

Как понять «в самой попытке присвоения»? Присвоение такому свойству значения это вызов обычной процедуры вида


HRESULT Xxxxxxx::put_Width(INT NewVal)
{
   ...
}

Это не опция.


Во-первых, контрол писали не вы. А другая фирма из другой страны, очень даже может быть. Контрол вам дали в форме .OCX-файла.


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


Это нужно будет менять CLSID-контрола, потому что в идеальном случае CLSID должен быть неким хешем идентичности класса и его поведения.


В третьих, может быть вообще и переписывать нечего и нет никого неадекватного. Контрол не может принять новый размер, потому что при попытке это сделать не хватило ресурсов. Вот он и честно сгенерировал ошибку. Что предложите в этом случае? Не генерировать ошибку, если HeapReAlloc сбойнул, а делать вид, что всё окей?

Использовать вместо него on error goto <номер строки> и обрабатывать по сути.

Не «номер строки», а метка (label). Просто номер строки является частным случаем метки, оставленным из соображений совместимости.


И, что характерно, обработчик ошибки, к которому перейдёт выполнение, имеет возможно сделать [Resume], [Resume Next] или ничего из этого.


Т.е. попросить «давай-ка попробуем сделать то же самое, но ещё раз — я вроде бы исправил первопричину неудачи», попросить «ладно, фиг с ним, что там у нас дальше», или же вообще перейти к совершенно иному плану действий.


Диалоге в стиле «Не удалось выполнить <....> Повторить / Пропустить / Отмена» реализуются в буквальном смысле в виде пары строк.


Предлагаю показать реализацию выполнения какой-то потенциально сбойной операции с показом диалога Повторить/Пропустить/Отмена при использовании try...catch-подхода. Как минимум, у вас будет ещё и цикл.

Типичный пример использования On Error Resume Next — это начинка обработчика события Resize формы, которое долго перемасштабировать начинку окна.


Например, у вас окно, всё пространство которого должен занимать TextBox, за исключением рамки в, скажем, 5 пикселей.


И вы пишите что-то вроде


   TB.Height = Me.ScaleHeight - Padding*2
   TB.Width = Me.ScaleHeight - Padding*2

При сворачивании окна или при его ресайзе таком, что окно будет иметь слишком маленький размер, у вас получатся отрицательные координаты. Что приведёт к ошибке, и ваше приложение сразу мигом завершится.


On Error Resume Next «улаживал» ситуацию. В случае try...catch-подхода пришлось бы каждую строку обрамить в try с пустым catch. Или вы можете сначала вычислять ширину/высоту, проверять, меньше ли она нуля, и если меньше, то не присваивать полунный результат свойству Height/Width, или же присваивать, но «клэмпнув» до нуля. Поздравляю, вы делаете двойную работу дважды: и кода у вас будет больше, и всё равно внутри set-хендлеров свойств Height/Width выше значение проверяет на <0, чтобы, в случае чего выкинуть исключение.


То есть и вы делаете проверку, и ваши проверенные данные всё равно второй раз подвергнут проверке, чтобы в случае чего выкинуть исключения. Так может быть лучше, раз вам в руки дали механизм реагирования на исключения, воспользоваться и сказать: «просто проигнорируй это»?


На самом деле, примеров может быть очень много. В GUI-программах очень много какого кода, если в нём происходит ошибка, может продолжать работать дальше. Во всяком случае, это лучше, чем если приложение молча вылетит.


Скажем, у вас какой-то многошаговый процесс, в ходе которого должен проиграться «бымц» и показаться сообщение. Мало ли, что может пойти не так: пусть лучше мы молча проглотим тот факт, что не получилось проигрять «бымц» или показать красивую плашку, чем программа рухнет из-за невозможности претворить в жизнь какую-нибудь свистиелку.


Прошу сейчас не передёргивать и не говорить, что при таком подходе может нарушиться бизнес-логика и пострадать данные. On Error Resume Next никогда не был дефолтным поведением. Это то, что программист сознательно выбирал или не выбирал для использования.


В GUI-приложениях, где много какого кода отвечает за обновление UI, за визуальные знаки, всплывающие подсказки, звуки, перекраивание UI — очень много случаев, когда ошибка не фатальна, и пусть лучше в приложении контролы не отмасштабируются, чем оно упадёт.


Если же вы предлагаете в случае ошибки выходить из процедуры, то, ещё раз напомнив, что VB не мешает вам поступать именно так, предложу такой пример: в окне формы 10 контроллов, которые надо масштабировать. Контроллы писал неизвестно кто, и каждый при попытки изменить его размеры может взбрыкнуть.


У вас ест процедура с примерно таким кодом:


ToolBar.Width = NewWidth
LogPane.Width = NewWidth
MainPanel.Width = NewWidth
SecondarySlider.Width = NewWidth
...
StatusBar.Width = NewWidth

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


Так вот, что вы выбираете?
1) Из-за облома в одном контроле (возможно облом был единократным), вся программа вылетает
2) Из-за того, что один контрол взбрыкнул, при изменении размеров окна все контролы перестают менять свои размеры, потому что кто-то в коде написал On Error Goto exit_sub
3) Только один косячный контрол перестаёт реагировать на изменение размеров окна, а остальные 9 ведут себя адекватно.


Какой подход больше всего обрадовал бы пользователя? Обеспечивал бы сохранность данные? А какой взбесил бы больше всего?

Эм, не согласен. В PHP @ значит не показывать ошибку, но не значит "пропустить строку в случае ошибки".

Если уж на то пошло, то и в VB конструкция «On Error Resume Next» не означает «пропустить строку в случае ошибки».

Information

Rating
105-th
Location
Петропавловск, Северо-Казахстанская обл., Казахстан
Registered
Activity

Specialization

Десктоп разработчик, Инженер встраиваемых систем
Pure C
Assembler
X86 asm
Win32 API
Visual Basic
MySQL
Git
ООП
Разработка электроники
Обратная разработка