6.2. Ключи компилятора

(Этот раздел про Free Pascal и Delphi. В Pascal ABC, подозреваю, многие ключи должны бы работать, но я не проверял; в C+++ есть похожая система директив препроцессора.)

Ключи компилятора позволяют влиять на поведение компилятора в отдельных случаях. Все ключи оформляются в виде строки {$...}, т.е. как обычный комментарий, но начинающийся с символа $. Большинство ключей могут быть просто или включены, или выключены, и, соответственно, пишутся, например {$R+,Q+,S+,I+,B-}; некоторые ключи имеют другой формат.

Каждому ключу соответствует некоторая опция в настройках оболочки (IDE) паскаля; тем не менее, ключи, указанные в файле с программой, имеют более высокий приоритет. Я считаю, что в начале программы ОБЯЗАТЕЛЬНО надо записать те ключи, которые вам важны (в первую очередь r, q, s, i, mode и возможно o), чтобы быть уверенными, что ваша программа будет у жюри компилироваться с теми же ключами, что и у вас.

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

Ключи $I, $R, $Q, $S включают/выключают различные проверки:

  • $I — проверку при чтении данных из файла. Если он включён ({$I+}), и вы хотите считать число из файла, а там написаны буквы, то ваша программа вылетит с Runtime Error. Если ключ выключен, то ошибки не произойдёт, но, конечно, считается некоторое, скорее всего, неправильное, число.
  • $R — проверку на соответствие работы с памятью размерам выделенной памяти. А именно, проверяется переполнение массива (верно ли, что при обращении к элементу массива вы не выходите за его границы), и при записи значения в каждую переменную происходит проверка, влезает ли значение переменной в память, выделенную под неё (не пытаетесь ли вы значение 1000 сохранить в переменную типа byte). Если ключ включён, то при обнаружении ошибки произойдёт Runtime Error (конкретно, Range Check Error — RE 201), и программа тут же завершит работу. Если выключен, то произойдёт неизвестно что. В частности, если вы пытались записать данные за пределы массива, и ключ выключен, то запись все равно произойдёт. При этом будет затёрто то значение, которое там лежало, а там могло лежать значение другой переменной или даже исполняемый код вашей программы. В результате дальнейшее поведение вашей программы предсказать почти невозможно, она начнёт делать неожиданные вещи, и может сложиться впечатление, что программа «сходит с ума». Поэтому, если программа делает что-то странное, проверьте, не связано ли это с переполнением массива — просто включите ключ $R, и, возможно, вы поймёте, в чем дело.
  • $Q — проверку на переполнение при промежуточных вычислениях (+, -, *, abs и т.п., но не inc и dec). Если у вас в программе написано некоторое сложное арифметическое выражение с целыми числами, то при промежуточных вычислениях может произойти переполнение: результат промежуточного вычисления может не влезть в память, в которой эти вычисления производятся. Если ключ $Q включён, то в таком случае случится Runtime Error (конкретно, Arifmetic Overflow — RE 215), и программа тут же завершится. Если выключен, то, скорее всего, невлезающие биты будут просто отброшены и вычисления будут продолжены с полученным неправильным значением. В частности, часто в результате в ответе получается отрицательное число, даже если оно там математически не могло получиться.
  • $S — проверку не переполнение стека. Если ключ включён, то при каждом вызове функции будет проверятся, достаточно ли стека и, если недостаточно, то будет Runtime Error (Stack Overflow — RE 202), и программа тут же завершится. Если ключ выключен, то проверки не делаются и, аналогично ключу $R, программа может серьёзно «сойти с ума».

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

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

В FP чтобы увидеть строку, на которой ошибка, надо перед запуском программы войти в режим отладки, т.е. нажать F8, и только после этого Ctrl-F9.

Более того, если вы даже не можете узнать, на какой строке ошибка (например, если ошибка произошла при тестировании задачи на сервере жюри), вы все равно узнаете, что произошла именно такая ошибка (вам сообщат RE, а не WA), и вы будете знать, что надо искать что-то подобное. Наоборот, если ключи выключены, то после ошибки программа спокойно себе будет работать дальше, но, скорее всего, уже с неправильными данными. В итоге проблема всплывёт совсем в другом месте или в другом обличии, и вы будете искать ошибку совсем не там или будете искать совсем не ту ошибку. Например, может оказаться, что в выходной файл выведено отрицательное число, при том, что должно быть положительное, или, например, вы будете ломали голову, почему на очередной строке изменяется значение переменной \(i\), при том, что она вообще в этой строке не упоминается, или при выводе ответа в файл вам напишут File not open for output, при том, что у вас все нормально с работой с файлами, или программа будет работать долго и зацикливаться и т.д. и т.п.

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

В общем, надо каждый раз думать головой, надо ли вам включать или выключать эти ключи (но в любом случае надо их или явно включить в программе, или явно выключить! чтобы не зависеть от того, какие настройки по умолчанию у жюри или у вашего компилятора). ИМХО, во время разработки программы ключи обязательно должны быть включены, чтобы вам легче было отлавливать ошибки на ваших же тестах. Когда же вы сдаёте программу, то тут надо думать.

В олимпиадах без фидбека логично выключать ключи перед сдачей программы на проверку: включённые ключи вам там уже не помогут, зато могут 1. затормозить программу и 2. может случайно получится, что с выключенными ключами программа правильно пройдёт тест, на котором при включённых ключах она вылетела бы с RE. В олимпиадах же с фидбеком, где вы можете видеть результаты тестирования вашей программы и анализировать их для исправления программы, может иметь смысл сдавать программы и с включёнными проверками: сообщение, что у вашей программы RE, вам поможет намного больше, чем WA.

Кстати, частая ошибка: если ваша программа вылетела с одной из этих Runtime Error, то кажется: давайте просто выключим соответствующий ключ, и ошибка исчезнет… нет! Срабатывание одного из этих ключей указывает на наличие серьёзной ошибки в коде. Отключая ключ, вы лишь убираете оповещение о ней, сама ошибка остаётся. Если произошла такая ошибка, надо разбираться, в чем дело и исправлять основной код программы!

В C/C++ нет общепринятых способов контроля подобных ошибок (поэтому, в частности, проблемы с переполнением являются одними из довольно частых и трудноуловимых ошибок в программе на C/C++). Конец части про эти четыре ключа.

Ключ $B влияет на обработку выражений типа

if (n>0)and(f(n)>0) then

т.е. когда у вас в if два или более условий, связанных оператором or или and. В таком случае после вычисления первого условия может оказаться, что от значения второго условия ничего не зависит (например, в приведённом примере: если \(n\leq0\), то проверять \(f(n)\) незачем: в любом случае if не выполнится). Если ключ $B выключен, то в подобных случаях второе условие проверяться не будет, иначе будет. Может показаться, что ключ не даёт никакого эффекта, кроме выигрыша/проигрыша во времени, но на самом деле это не так. Второе условие может иметь какие-нибудь побочные эффекты и тогда программа будет работать по-разному в зависимости от ключа $B; например, если, как в примере выше, у вас во вторых скобках идёт вызов функции, то при \(n\leq0\) она будет или не будет вызвана в зависимости от ключа $B — а уж функция может делать очень много что. Может показаться странным, но в большинстве случаев вам нужно как раз поведение, соответствующее отключённому ключу, $B- — действительно, такой if, как выше, вы, возможно, написали потому, что ваша функция плохо обрабатывает значения \(n\leq0\) — значит, её действительно не надо вызывать в таком случае. Ещё два примера, когда ключ $B важен:

if (n>0)and(n<maxn)and(a[n]=k) then
if (x>=0)and(sqrt(x)<y) then

Можете подумать, какие тут возникнут проблемы, если этот код будет скомпилирован с $B+.

Ключ $A влияет на так называемое «выравнивание» (alignment). Современные микросхемы ОЗУ так устроены, что доступ к паре байт происходит быстрее или медленнее, в зависимости от того, начинается эта пара с чётного или с нечётного байта. Аналогично время доступа к четвёрке байт может зависеть от остатка от деления на 4 номера первого байта четвёрки и т.п. Поэтому для оптимизации времени доступа компиляторы могут выполнять «выравнивание» данных, т.е. делать так, чтобы все переменные начинались с адресов, кратных 4 (или 2, или 8, или 16, или размеру самой переменной, в зависимости от настроек), при необходимости оставляя неиспользуемое пространство (байты) между соседними переменными. Аналогично могут выравниваться все поля в record’ах и т.п. На это выравнивание и влияет ключ $A. Если ключ выключен, то никакого выравнивания не выполняется, переменные и поля в record’ах идут подряд, но время доступа к ним получается больше. Если ключ включён, то происходит некоторое выравнивание по умолчанию, см. подробнее в справке. Можно также явно указать требуемое выравнивание, указав конкретное число ({$A8} и т.п.), подробнее тоже см. в справке. Но как правило достаточно значения по умолчанию.

Ключ $O включает или отключает различные оптимизации, которые может применять компилятор. В режиме {$o+} программа работает быстрее, порой ощутимо быстрее, но отлаживать ее намного тяжелее.

Ключ $mode во Free Pascal устанавливает режим работы компилятора, меняя разные его параметры: например, поведение типа integer, типа string, наличие переменной result в функциях и т.п. Всегда рекомендую писать {$mode delphi}, если вы только уверенно не понимаете, почему стоит писать как-нибудь еще. Режим delphi делает integer 4-байтовым, делает string произвольной длины (а не только до 256 символов), позволяет использовать переменную result в функциях, и т.д.

Конец описания различных ключей компилятора.

Ещё скажу про так называемые «conditional defines». В программе можно использовать также ключи компилятора $define, $undef и $ifdef/$else/$endif. Смысл вот в чем: при чтении исходного текста вашей программы в каждый момент у компилятора есть некоторый набор, условно говоря, «опций условной компиляции». Каждый «опция» — это просто произвольная строка из латинских букв. Никакого глубокого смысла у них изначально нет, вы используете эти опции так, как вам нужно будет.

А именно, ключ $define позволяет добавить опцию к этому списку опций: {$define optname} добавляет опцию optname и т.п. Ключ $undef — удалить опцию из списка (если она там есть): {$undef optname}. А теперь главное: ключи $ifdef/$else/endif позволяют проверять, определена ли та или иная опция: а именно, если в коде есть последовательность

{$ifdef optname} some code {$else} other code {$endif}

то, если опция optname входит в этот список к моменту, когда компилятор читает команду ifdef (а читает он файл-программу посимвольно от начала к концу), то этот кусок кода будет иметь абсолютно такой же смысл, как будто на его месте просто находился код some code, иначе — как будто на его месте находился код other code. Часть else, конечно, может быть опущена.

Примеры:

{$ifdef check} {$r+,q+,s+,i+} {$else} {$r-,q-,i-,s-} {$endif}

— если опция check «включена», то включает все проверки, иначе отключает. Например, можно написать в начале программы

{$define check} {$ifdef check} {$r+,q+,s+,i+} {$else} {$r-,q-,s-,i-} {$endif}

— и все проверки будут включены. Вы пишите программу, тестируете её, а перед сдачей на проверку добавляете один пробел в этот код:

{ $define check} {$ifdef check} {$r+,q+,s+,i+} {$else} {$r-,q-,s-,i-} {$endif}

— строка $define check превращается в обычный комментарий (а не директиву компилятора :) ) — и все проверки отключаются.

Ещё пример:

{$ifdef debug} writeln(output,i,' ',j,' ',a[i,j]);{$endif}

позволяет аналогичным образом управлять отладочным выводом.

Ещё пример:

{$ifdef for}for i:=1 to 10 do{$endif}
  writeln('!');

в зависимости от состояния опции будет либо цикл, либо одиночная команда.

Достоинство этого в том, что просто добавляя/убирая пробел (или любой другой символ) в начале директивы define, вы сразу изменяете поведение программы. (Например, у вас в программе есть код для отладочного вывода. Чтобы его убрать, вообще говоря нужно закомментировать все его строки. Но если отладочный вывод оформлен так, как я показал выше, то достаточно добавить в программу один символ.)

Ещё одно достоинство: изначально, до начала чтения программы компилятором, список опций не обязательно пуст; оно определяется строкой компиляции или настройками среды разработки. Например, вы можете настроить свой локальный компилятор так, чтобы у вас были включены определенные опции, а у жюри они будут выключены. Например, я пользовался следующим приёмом: настраивал опцию check в настройках локального компилятора, а в начале программы писал

{$ifdef check} {$r+,q+,s+,i+} {$else} {$r-,q-,i-,s-} {$endif}

— и у меня из оболочки компилилось с включёнными ключами, а у жюри — с выключенными :). Правда, в этом тоже были свои неудобства…

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

Ещё раз подчёркиваю, что это все влияет только на компилятор. Поэтому, например, писать типа

for i:=1 to n do begin
  {$ifdef check}
  writeln('!');
  {$endif}
  {$define check}
end;

и рассчитывать, что на второй и далее итерациях цикла check будет определена, бессмысленно: компилятор читает входной файл по порядку, не обращая внимания на циклы и т.д., и просто не включит код writeln('!') в exe’шник.

И, наконец, ещё раз. Конечно, не используйте это все, если вы это не понимаете. Сначала поймите, потестируйте, проверьте, что вы это правильно понимаете, а потом только используйте.