Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

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 – отличная штука, но только в малых дозах. Иначе программа превращается в настоящее спагетти,​ которое практически невозможно распутать.
 +
 +