Различия
Здесь показаны различия между двумя версиями данной страницы.
— |
articles:c-tricks-1fh [2017/09/05 02:55] (текущий) |
||
---|---|---|---|
Строка 1: | Строка 1: | ||
+ | ====== c-tricks-1Fh ====== | ||
+ | <sub>{{c-tricks-1Fh.odt|Original file}}</sub> | ||
+ | |||
+ | ====== сишные трюки\\ (1Fh выпуск) ====== | ||
+ | |||
+ | крис касперски ака мыщъх, a.k.a. souriz, a.k.a. nezumi, no-email | ||
+ | |||
+ | **язык си не предоставляет никаких средств для временного отключения блоков кода и большинство программистов делают это с помощью комментариев. казалось бы что может быть проще и о каких трюках тут вообще говорить? на самом же деле, комментарии не только не единственный, но и едва ли не самый худший прием среди прочих о которых мы сейчас и поговорим!** | ||
+ | |||
+ | ===== трюк #1 – комментарии, ремарки и помарки ===== | ||
+ | |||
+ | Системы контроля версий как раз и создавались для того, чтобы обеспечить легкий, прозрачный и непротиворечивый механизм безопасной правки исходных текстов инвариантный по отношению к самому языку. Однако, на практике системы контроля версий используются только для организации совместной работы над проектом, да и то не всегда. Уж слишком много телодвижений приходится совершать всякий раз, а программисты — люди ленивые. | ||
+ | |||
+ | Если нам необходимо временно отключить блок кода, намного проще закомментировать его, а потом удалить комментарии, подключая его обратно. Быстро. Дешево. Сердито. Но увы… потенциально небезопасно с точки зрения внесения новых ошибок и развала уже отлаженной программы, чего допускать ни в коем случае нельзя. А потому прежде, чем идти дальше, сформулируем перечень требований, предъявляемый к механизмам отключения кода: | ||
+ | |||
+ | - легкость использования (никто не будет пользовать средство, требующее кучи телодвижений); | ||
+ | - вложенность (внутри отключаемого блока может находится один или несколько ранее отключенных блоков); | ||
+ | - многоуровневость (если для отключения блока кода необходимо исправить два и более несмежных фрагментов исходного текста, необходимо гарантировать корректное снятие блокировки, что становится особенно актуально, если отключаются независимые блоки А, B, С – тогда, при включении блока B возникает угроза подключения фрагментов, относящихся к блокам A и C, что ведет к развалу программы); | ||
+ | - поддержка всех языковых конструкций (какой прок от инструмента, если он работает только с ограниченным набором языковых конструкций, например, не позволяет отключать ассемблерные вставки?!); | ||
+ | Удовлетворяют ли комментарии указанным требованиям?! А вот и нет! Комментарии в стиле Си (/* */) очень удобны, поскольку, позволяют отключать огромные блоки кода нажатием всего четырех клавиш, к тому же они могут располагаться в любом месте строки, а не только в ее начале. Однако, отсутствие поддержки вложенности создает серьезные проблемы. | ||
+ | |||
+ | Например: | ||
+ | |||
+ | /* ошибка! закомментированный блок уже содержит /* */ | ||
+ | |||
+ | for (a = 0; a < N; a++) | ||
+ | |||
+ | { | ||
+ | |||
+ | /* | ||
+ | |||
+ | for (b = 0; b < M; b++) | ||
+ | |||
+ | if (!strcmp(name_array[a], vip_array[b])) continue; | ||
+ | |||
+ | */ | ||
+ | |||
+ | |||
+ | |||
+ | // DeleteFile(name_array[a]); | ||
+ | |||
+ | pritnf("%d %s\n", a, name_array[a]); | ||
+ | |||
+ | |||
+ | |||
+ | } | ||
+ | |||
+ | */ | ||
+ | |||
+ | Листинг 1 демонстрация некорректного использования комментариев /* */ для временного отключения блоков кода | ||
+ | |||
+ | Попытка выключить цикл for (a,,) ведет к ошибке компиляции — комментарии /* */ не могут быть вложенными и в таких случаях программисты используют альтернативу в виде "//" допускающую вложенность, но, увы, вручную проставляемую вначале _каждой_ строки, что очень утомительно и совершенно непроизводительно, если, конечно, не использовать макросы, поддерживаемые средой разработки (а практически все среды разработки их поддерживают). Аналогичным образом осуществляется и снятие комментариев. | ||
+ | |||
+ | И все было бы хорошо, да вот неоднозначности с уровнем вложенности делают отключение блоков небезопасным.В нашем случае мы имеем три раздельных отключаемых блока кода. Во-первых, это заблокированная проверка принадлежности удаляемого файла к vip_array, во-вторых, это, собственно, само удаление файла (заблокированное и замененное отладочной печатью через printf) и, в-третьих, комментарий, пытающийся отключить цикл for(a,,) со всем что в нем находится. | ||
+ | |||
+ | Отключаются блоки кода очень просто, а вот обратное утверждение уже неверно. Никаким автоматизмом тут уже и не пахнет, в результате чего нам приходится разбираться с назначением каждого блока самостоятельно. Однако, если немного поколдовать над комментариями… | ||
+ | |||
+ | Пусть следом за "//" идет цифра (или буква) указывающая принадлежность текущей комментируемой строки к блоку кода. Продвинутые среды разработки типа Microsoft Visual Studio поддерживают развитый макроязык, позволяющий выполнять лексический анализ, удаляя только те комментарии, за которыми идет заданная буква/цифра. | ||
+ | |||
+ | Это может выглядеть, например, так: | ||
+ | |||
+ | //3for (a = 0; a < N; a++) | ||
+ | |||
+ | //3{ | ||
+ | |||
+ | //3 //2 | ||
+ | |||
+ | //3 //2for (b = 0; b < M; b++) | ||
+ | |||
+ | //3 //2if (!strcmp(name_array[a], vip_array[b])) continue; | ||
+ | |||
+ | //3 //2 | ||
+ | |||
+ | //3 //1// DeleteFile(name_array[a]); | ||
+ | |||
+ | //3pritnf("%d %s\n", a, name_array[a]); | ||
+ | |||
+ | //3 } | ||
+ | |||
+ | Листинг 2 имитация многоуровневой структуры отключаемых блоков исходного кода посредством комментариев | ||
+ | |||
+ | Проблема вложенности решена на 100%, проблема многоварианости — на 50% (после удаления комментария //1 мы так же должны удалить, а точнее временно заблокировать следующую за ним строку с отладочной печатью), однако, в целом предложенная техника намного более удобна и единственный серьезный недостаток — привязка программиста к конкретной среде с набором пользовательских макросов. Менее серьезный недостаток — ассемблерные вставки как правило не поддерживают Си/Си++ комментариев и потому должны обрабатываться отдельно, усложняя реализацию нашего макродвижка и сводя его преимущества на нет. | ||
+ | |||
+ | ===== трюк #2 — директивы условной трансляции ===== | ||
+ | |||
+ | Разработанные для поддержки многовариантного кода директивы условной трансляции оказались практически невостребованными (речь, разумеется, идет только о временном выключении кода), что очень странно — директивы условной трансляции намного более эффективны, чем комментарии и пример, приведенный ниже, доказывает этот тезис. | ||
+ | |||
+ | #define _D1_// блок _D1_ включен | ||
+ | |||
+ | //#define _D2_// блок _D2_ выключен | ||
+ | |||
+ | #define _D3_// блок _D3_ включен | ||
+ | |||
+ | #ifdef _D1_ | ||
+ | |||
+ | for (a = 0; a < N; a++) | ||
+ | |||
+ | { | ||
+ | |||
+ | #ifdef _D2_ | ||
+ | |||
+ | for (b = 0; b < M; b++) | ||
+ | |||
+ | if (!strcmp(name_array[a], vip_array[b])) continue; | ||
+ | |||
+ | #endif | ||
+ | |||
+ | #ifdef _D3_ | ||
+ | |||
+ | DeleteFile(name_array[a]); | ||
+ | |||
+ | #else | ||
+ | |||
+ | pritnf("%d %s\n", a, name_array[a]); | ||
+ | |||
+ | #endif | ||
+ | |||
+ | } | ||
+ | |||
+ | #endif | ||
+ | |||
+ | Листинг 3 директивы препроцессора, отключающие блоки кода | ||
+ | |||
+ | Проблема вложенности решается сама собой, многовариантность поддерживается очень хорошо, позволяя нам включать/выключать определенные блоки, не затрагивая остальных, причем, при подключении "DeleteFile(name_array[a])" — автоматически отключается отладочная печать и наоборот. В результате чего риск развала программы уменьшается до нуля. Самое интересное, что директивы условной трансляции ничуть не хуже работают и с ассемблерными вставками! | ||
+ | |||
+ | __asm{ | ||
+ | |||
+ | xor eax,eax | ||
+ | |||
+ | #ifdef _D1_ | ||
+ | |||
+ | PUSH file_name | ||
+ | |||
+ | CALL DeleteFile | ||
+ | |||
+ | #endif | ||
+ | |||
+ | } | ||
+ | |||
+ | Листинг 4 директивы препроцессора, отключающие ассемблерные инструкции _внутри_ ассемблерных вставок | ||
+ | |||
+ | Конечно, писать "#if def _Dx_" намного длиннее, чем "//" или "/* */", однако, это не проблема — клавиатурные макросы на что?! Хотя про нежелание связаться с макросами мы уже говорили. Ну макросы это ладно. Хуже всего, что отключенные блоки кода не попадают в релиз, и если у конечного пользователя программа начнет дико глючить у нас не будет никакой возможности отключить их без перекомпиляции всего кода. | ||
+ | |||
+ | ===== трюк #3 – ветвления ===== | ||
+ | |||
+ | Финальный прием устраняет основные недостатки предыдущего трюка, добавляя к нему свои собственные достоинства, а достоинств у него… Короче, намного больше одного. Идея заключается в использовании конструкции if (_Dx_), а при необходимости и if (_Dx_) else. | ||
+ | |||
+ | Оператор "if", стоящий перед одиночным блоком кода, не требует замыкающего "#endif", что ускоряет процесс программирования и не так сильно загромождает листинг. Но это мелочь. Гораздо важнее, что если _Dx_ константа (например, "1"), то оптимизирующий компилятор выбрасывает вызов if, удаляя лишний оверхид. Если же _Dx_ переменная (глобальная, конечно), то компилятор оставляет ветвление "как есть", давая нам возможность управлять поведением программы — если у пользователей возникнут проблемы из-за ошибки в плохо отлаженном блоке кода, то этот блок можно отключить (естественно, если значения флагов вынесены в конфигурационный файл или доступны через пользовательский интерфейс, но это уже несущественные детали реализации). | ||
+ | |||
+ | Пример использования ветвлений для отключения блоков кода приведен ниже: | ||
+ | |||
+ | #define _D1_0// блок _D1_ выключен (ветвление в релиз не попадает) | ||
+ | |||
+ | #define _D3_1// блок _D3_ включен (ветвление в релиз не попадает) | ||
+ | |||
+ | int_D2_1// блок _D2_ включен (ветвление попадает в релиз!) | ||
+ | |||
+ | if (_D1_) | ||
+ | |||
+ | for (a = 0; a < N; a++) | ||
+ | |||
+ | { | ||
+ | |||
+ | |||
+ | |||
+ | if (_D2_) | ||
+ | |||
+ | for (b = 0; b < M; b++) | ||
+ | |||
+ | if (!strcmp(name_array[a], vip_array[b])) continue; | ||
+ | |||
+ | if (_D3_) | ||
+ | |||
+ | DeleteFile(name_array[a]); | ||
+ | |||
+ | else | ||
+ | |||
+ | pritnf("%d %s\n", a, name_array[a]); | ||
+ | |||
+ | } | ||
+ | |||
+ | Листинг 5 использование ветвлений для выключения блоков кода | ||
+ | |||
+ | Как мы видим, листинг 5 намного компактнее и нагляднее листинга 4, так что при всем уважении к директивам условной трансляции, они идут лесом. А вот ветвления можно использовать для выключения блока ассемблерных вставок (о чем кстати говоря, умалчивает штатная документация, но следующий пример компилируется вполне нормально): | ||
+ | |||
+ | #define _D1_0 | ||
+ | |||
+ | if (_D1_) | ||
+ | |||
+ | __asm{ | ||
+ | |||
+ | INT 03 | ||
+ | |||
+ | } | ||
+ | |||
+ | Листинг 6 использование ветвлений для выключения ассемблерных вставок | ||
+ | |||
+ | Ветвления, конечно, тоже не лишены недостатков, однако, для временного выключения блоков кода они намного лучше, удобнее и продуктивнее, чем комментарии. Естественно, существуют и другие средства. Взять хотя бы "return", позволяющий одним движением руки погасить блок кода до самого конца функции. Критикуемый GOTO – отличная штука, но только в малых дозах. Иначе программа превращается в настоящее спагетти, которое практически невозможно распутать. | ||
+ | |||
+ | |||