Проверки времени выполнения и Constraint_Error
Constraint_Error — страж границ: Ada по умолчанию проверяет диапазоны, индексы и переполнения, превращая «тихую» порчу памяти в честное исключение.
Предопределённые проверки (run-time checks) — встроенные в язык контроли времени выполнения (границы массивов, диапазоны подтипов, переполнение, разыменование
null), нарушение которых возбуждает стандартные исключения, прежде всегоConstraint_Error.
То, что в C приводит к неопределённому поведению и тихому разрушению памяти, в Ada по умолчанию ловится. Выход за границу массива, присваивание значения вне диапазона подтипа, переполнение при сложении, обращение по null-ссылке — каждая такая ситуация порождает не «случайный байт где-то рядом», а конкретное исключение в конкретной точке. Это и есть один из главных секретов репутации Ada как «безопасного» языка: безопасность не приклеена снаружи статическим анализатором, а встроена в семантику исполнения.
Какие проверки выполняет язык
Стандарт перечисляет несколько категорий проверок. Большинство из них при нарушении дают Constraint_Error: проверка диапазона (range check) — значение не помещается в диапазон подтипа; проверка индекса (index check) — индекс выходит за границы массива; проверка длины (length check) — несовпадение длин при присваивании массивов; проверка деления на ноль; проверка переполнения (overflow check) для целочисленной арифметики; проверка доступа (access check) — разыменование null. Отдельные ситуации дают другие исключения: исчерпание памяти — Storage_Error, нарушение порядка elaboration или иной семантический сбой — Program_Error.
declare
subtype Percent is Integer range 0 .. 100;
P : Percent;
Arr : array (1 .. 5) of Integer := (others => 0);
K : Integer := 7;
begin
P := 150; -- range check: 150 вне 0..100 → Constraint_Error
-- или:
Arr (K) := 1; -- index check: 7 вне 1..5 → Constraint_Error
exception
when Constraint_Error =>
Put_Line ("Нарушение ограничения перехвачено");
end;
Обратите внимание на роль подтипов (subtypes). Объявив subtype Percent is Integer range 0 .. 100;, вы не просто документируете намерение — вы заставляете язык проверять его при каждом присваивании. Подтип с диапазоном — это контракт на значение, который компилятор и среда исполнения вместе удерживают. Поэтому в Ada так ценят узкие, осмысленные подтипы: они переносят логические ограничения предметной области («процент — это 0..100», «возраст — 0..150») прямо в систему типов.
Проверка переполнения: важное отличие от C
В большинстве системных языков целочисленное переполнение либо «оборачивается» по модулю, либо вовсе является неопределённым поведением — и в обоих случаях молча даёт неверный результат. Ada по умолчанию проверяет переполнение для своих знаковых целочисленных типов: если результат сложения не помещается в диапазон типа, возбуждается Constraint_Error. Это превращает класс трудноуловимых, зависящих от данных ошибок в немедленный, локализованный сбой.
declare
X : Integer := Integer'Last; -- максимум типа Integer
begin
X := X + 1; -- overflow check → Constraint_Error
exception
when Constraint_Error =>
Put_Line ("Переполнение целого перехвачено");
end;
Когда нужна именно модульная («оборачивающаяся») арифметика — например, для хеширования или работы с регистрами, — Ada предоставляет модульные типы: type Byte is mod 256;. Для них переполнение определено как взятие по модулю и не является ошибкой. Так язык разделяет два разных намерения: «арифметика, где переполнение — ошибка» (обычные целые) и «арифметика по модулю» (модульные типы), не позволяя их перепутать.
Атрибуты как инструмент защиты
Чтобы не доводить до исключения, в Ada принято спрашивать у типа его границы и характеристики через атрибуты: T'First, T'Last, T'Range, A'Length, T'Valid. Цикл по массиву пишут как for I in A'Range loop — это исключает ошибку индекса в принципе, ведь диапазон берётся у самого массива. Атрибут 'Valid позволяет проверить, корректно ли значение объекта (актуально для данных, пришедших извне, из ввода или аппаратуры).
-- Безопасный обход без риска выйти за границы:
for I in Arr'Range loop -- диапазон гарантированно совпадает с массивом
Arr (I) := Arr (I) + 1;
end loop;
-- Проверка пришедшего извне значения перед использованием:
if Raw'Valid then
Process (Raw);
else
Put_Line ("Получено некорректное значение");
end if;
Как работает под капотом
Компилятор вставляет проверки в машинный код там, где они нужны, но не везде слепо: если он может доказать, что нарушение невозможно (индекс заведомо в пределах 'Range, значение заведомо в диапазоне), проверка опускается как избыточная. Так платится только за то, что действительно нужно контролировать. Для предельно горячих участков или сертифицированных конфигураций проверки можно отключить директивой pragma Suppress — но это сознательный шаг, снимающий гарантию, и в ответственных системах его делают точечно и осознанно, а не «ради скорости вообще». Философия Ada здесь: безопасно по умолчанию, быстро — по явному разрешению. Это зеркальная противоположность C, где быстро (и опасно) по умолчанию, а безопасность надо добывать вручную.
Подтипы как документация и как контроль
Стоит глубже осознать двойную роль подтипов, потому что она пронизывает весь стиль программирования на Ada. Объявляя subtype Temperature is Integer range -50 .. 150;, инженер делает сразу два дела. Во-первых, он документирует намерение прямо в системе типов: всякий, кто читает код, видит допустимый диапазон без комментариев и без чтения тел подпрограмм. Во-вторых, он включает автоматический контроль: любое значение, выходящее за пределы, будет отвергнуто в момент присваивания. Документация и проверка здесь не два разных артефакта, способных рассинхронизироваться, а одно и то же объявление — оно не может «соврать», потому что само же и проверяется.
Это меняет экономику надёжности. В языках без диапазонных подтипов проверки «значение в допустимых пределах» приходится писать руками в каждой точке, где данные приходят извне, и легко какую-то точку пропустить. Ada переносит проверку к самому типу: достаточно объявить подтип один раз, и контроль применяется везде, где этот подтип используется, без дублирования кода. Опытные инженеры на Ada создают богатую иерархию узких подтипов предметной области — Pressure, Angle, Channel_Id, — и тем самым переносят значительную часть логики корректности из исполняемого кода в декларации типов, где её невозможно забыть применить.
Чем дороже обходится отсутствие проверок
Чтобы прочувствовать ценность встроенных проверок, полезно вспомнить громкие катастрофы, корни которых — именно в неконтролируемых границах и переполнениях. Самый известный пример из мира Ada назидателен наоборот: при первом пуске ракеты Ariane 5 в 1996 году переполнение при преобразовании 64-битного числа с плавающей точкой в 16-битное целое привело к исключению, которое не было обработано, — и ракета была потеряна. Урок здесь тонкий: язык обнаружил проблему (проверка сработала и возбудила исключение — ровно как задумано), но переиспользованный без переанализа код не предусматривал её обработки в новом контексте. Это подчёркивает обе стороны медали: встроенные проверки честно ловят ошибку, но инженер обязан решить, что делать при её срабатывании. Молчаливое переполнение C просто дало бы неверное число и, возможно, ту же катастрофу — но без всякого сигнала, что что-то пошло не так. Ada хотя бы превращает скрытую ошибку в явное, диагностируемое событие, оставляя шанс обработать его осознанно.
Модульные типы и осознанный выбор арифметики
Стоит задержаться на различии между обычной и модульной арифметикой, потому что оно прекрасно иллюстрирует философию Ada «явно выражать намерение». Когда инженер пишет X : Integer, он заявляет: «это величина, для которой переполнение — ошибка». Когда он пишет type Hash is mod 2**32, он заявляет нечто иное: «это величина, живущая по модулю, где перенос за границу — нормальное, ожидаемое поведение». Язык удерживает эти два намерения раздельно и не даёт их перепутать: к модульному типу применима «оборачивающаяся» арифметика без исключений, к обычному — арифметика с контролем переполнения.
Это разделение устраняет целый класс недопониманий. В C один и тот же тип unsigned int используют и как «обычное число», и как «битовую маску по модулю», и компилятор не знает, какое поведение вы имели в виду, — переполнение всегда молча оборачивается, даже когда это ошибка. Ada заставляет сделать выбор явным в объявлении типа, и дальше следит за его соблюдением. Модульные типы незаменимы там, где модульность — суть задачи: хеш-функции, контрольные суммы, циклические счётчики, работа с аппаратными регистрами, криптография. А обычные целые с контролем переполнения защищают всю остальную арифметику от тихих ошибок. Выбирая тип, инженер на Ada фактически документирует семантику величины — и получает соответствующую проверку бесплатно.
Закрепим связь проверок с типами как стержень безопасности Ada. Сила языка не в том, что он «много проверяет в рантайме», а в том, что он позволяет выразить ограничение один раз — в типе — и проверять его везде автоматически. Узкий подтип range, модульный тип, предикат подтипа — всё это способы закодировать правило предметной области в системе типов, после чего язык удерживает его за вас при каждом присваивании, без дублирования проверок по коду. Это переносит ответственность за корректность данных с дисциплины программиста (которая подводит) на компилятор и runtime (которые не забывают). В сочетании с исключениями, делающими любое нарушение явным, получается эшелонированная защита, где ошибки ловятся максимально близко к месту возникновения. Привычка проектировать богатую систему точных типов вместо россыпи безымянных целых — один из главных навыков, которые воспитывает Ada и которые остаются полезны в любом языке.
Частые ошибки
- Считать, что переполнение «само обернётся». Для обычных целочисленных типов Ada переполнение — это
Constraint_Error, а не молчаливое обнуление; для модульной арифметики объявляйтеmod-тип явно. - Писать циклы с «магическими» границами.
for I in 1 .. 5при изменении массива рассинхронизируется;for I in Arr'Rangeвсегда верен. - Отключать проверки
pragma Suppressбез нужды. Это снимает страховку именно там, где ошибки опаснее всего. Отключайте точечно и только доказав корректность. - Игнорировать узкие подтипы. Если переменная по смыслу — процент или индекс, объявите подтип с диапазоном: язык будет ловить выход за пределы за вас.
Итоги
- Ada по умолчанию проверяет диапазоны, индексы, длины, деление на ноль, переполнение и разыменование
null. - Нарушение чаще всего даёт
Constraint_Errorв точной точке — никакой тихой порчи памяти. - Узкие подтипы (
range) переносят ограничения предметной области в систему типов и контролируются языком. - Целочисленное переполнение — ошибка; модульная арифметика — отдельные
mod-типы. - Атрибуты (
'Range,'Length,'Valid) предотвращают ошибки заранее; компилятор опускает доказуемо лишние проверки, аpragma Suppressснимает их осознанно.