Явные интерфейсы: почему компилятор должен видеть сигнатуру
Явный интерфейс — это полная сигнатура процедуры, известная компилятору в точке вызова; без него многие современные возможности языка просто не работают.
Явный интерфейс (explicit interface) — ситуация, когда в месте вызова процедуры компилятор знает её точную сигнатуру (типы, ранги, намерения, опциональность аргументов) и может проверить корректность вызова и правильно его сгенерировать.
Две модели вызова и откуда взялась проблема
Fortran 77 вызывал внешние процедуры «вслепую». Компилятор, встретив call solve(a, b, n), не знал ничего о solve кроме имени: ни сколько у неё аргументов, ни какого они типа. Он просто передавал то, что дал программист, по соглашению о вызовах. Если типы не совпадали — например, в solve второй аргумент целый, а вы передали вещественный — никакой диагностики не было, программа считала мусор или падала. Это и есть неявный интерфейс: контракт между вызывающим и вызываемым нигде не зафиксирован, ответственность целиком на человеке.
Современный Fortran ввёл понятие явного интерфейса. Когда компилятор в точке вызова знает полную сигнатуру, он проверяет каждый аргумент и, что не менее важно, может реализовать передачу сложных сущностей — ассумированных по форме массивов, опциональных аргументов, аллоцируемых и указателей. Без явного интерфейса эти механизмы передать невозможно технически, потому что они требуют скрытой передачи дескрипторов (формы массива, признака наличия и т.п.), а «слепой» вызов их не предусматривает.
Когда интерфейс уже явный — бесплатно
Хорошая новость: если вы пишете процедуры внутри модуля (после contains), их интерфейс автоматически явный для всех, кто делает use этого модуля. То же верно для внутренних процедур (объявленных после contains внутри программы или другой процедуры). Поэтому модульный стиль, к которому склоняет современный Fortran, попутно решает проблему интерфейсов: вы почти не пишете блоков interface вручную, потому что почти весь код живёт в модулях.
module linalg
implicit none
contains
! интерфейс этой процедуры явный для всех, кто use linalg
subroutine scale_vector(v, factor)
real, intent(inout) :: v(:) ! ассумированная форма требует явного интерфейса
real, intent(in) :: factor
v = v * factor
end subroutine scale_vector
end module linalg
Аргумент v(:) — массив ассумированной формы: процедура получает не только данные, но и сведения о размере через скрытый дескриптор. Это работает только потому, что интерфейс явный. Попробуйте вызвать такую процедуру из кода без use linalg — компилятор не разрешит, потому что не знает, как корректно передать v(:).
Блок interface для внешних процедур
Иногда модульного определения нет: процедура написана на C, лежит в чужой библиотеке без .mod-файла, или это легаси-код в отдельном файле. Тогда явный интерфейс описывают вручную — блоком interface. Внутри него повторяют сигнатуру процедуры (заголовок и объявления аргументов), но без тела.
program use_external
implicit none
interface
subroutine legacy_solve(a, n, x)
real, intent(in) :: a(n, n)
integer, intent(in) :: n
real, intent(out) :: x(n)
end subroutine legacy_solve
end interface
real :: matrix(3, 3), rhs(3)
! ... заполнение ...
call legacy_solve(matrix, 3, rhs)
end program use_external
Теперь компилятор знает сигнатуру legacy_solve и проверит вызов, хотя тело процедуры лежит в другом файле. Блок interface — это обещание: «процедура с таким именем существует и выглядит вот так». Если обещание не соответствует реальной процедуре, ошибка проявится при линковке или, хуже, в рантайме, поэтому интерфейс нужно держать в точности синхронным с телом. Именно поэтому ручные блоки interface считаются последним средством: когда можно, кладите процедуру в модуль и получайте интерфейс автоматически и без риска рассинхрона.
Что именно даёт явный интерфейс
Перечислим возможности, которые без явного интерфейса недоступны — это и есть прагматический ответ на вопрос «зачем он нужен»:
| Возможность | Почему нужен явный интерфейс |
Массивы ассумированной формы x(:) | Передаётся скрытый дескриптор формы |
Опциональные аргументы optional | Передаётся скрытый признак наличия |
Аргументы allocatable / pointer | Передаётся дескриптор с метаданными выделения |
Проверка intent(in/out/inout) | Компилятор сверяет намерение с использованием |
Аргументы с keyword= | Нужно знать имена формальных аргументов |
| Функции, возвращающие массив/тип | Нужно знать форму результата |
Заметьте: почти всё «современное» в передаче аргументов попадает в этот список. Старые процедуры с массивами явной формы a(n,n) и скалярами ещё работали при неявном интерфейсе, но как только вы переходите на удобные ассумированные массивы и опциональные параметры, явный интерфейс становится обязательным.
Как работает под капотом
Под «скрытым дескриптором» понимается дополнительная структура данных, которую компилятор незаметно передаёт вместе с массивом ассумированной формы: указатель на начало, размеры по каждому измерению, шаги (strides). Благодаря этому процедура внутри узнаёт size(v), lbound, ubound без отдельного аргумента-размера. Для опционального аргумента передаётся флаг «присутствует/отсутствует», который читает встроенная функция present(). Всё это часть бинарного контракта, и обе стороны — вызывающая и вызываемая — обязаны его соблюдать. Явный интерфейс ровно об этом: он сообщает вызывающей стороне, какой именно контракт нужен, чтобы корректно собрать вызов.
Внутренние и внешние процедуры: где какой интерфейс
Чтобы окончательно уложить тему, полезно классифицировать все виды процедур Fortran по тому, какой у них интерфейс. Модульные процедуры (после contains в модуле) — явный интерфейс для всех, кто делает use. Внутренние процедуры (после contains внутри программы или другой процедуры) — явный интерфейс в пределах объемлющей единицы, плюс они видят её локальные переменные (host association). Процедуры-аргументы, передаваемые в другую процедуру, требуют объявления через интерфейс или атрибут procedure. И наконец, внешние процедуры (отдельные subroutine/function вне всякого модуля) — по умолчанию неявный интерфейс, и именно для них приходится писать блок interface вручную, если нужна современная передача аргументов. Практический вывод прост: размещайте процедуры в модулях. Тогда вопрос интерфейсов отпадает сам собой — он всегда явный, всегда проверяемый, всегда синхронный с реализацией. Внешние процедуры в новом коде — это почти всегда либо интероп с C, либо вынужденная работа с легаси.
Процедурные указатели и абстрактные интерфейсы
Явные интерфейсы нужны не только для проверки вызовов, но и для более тонких механизмов — в частности, для процедурных указателей. Иногда требуется передать процедуру как данные: например, универсальный интегратор должен принять подынтегральную функцию, а решатель — функцию правой части дифференциального уравнения. Fortran позволяет объявить указатель на процедуру, но для этого нужно описать сигнатуру той процедуры, на которую он может указывать. Делается это блоком abstract interface — он задаёт «форму» процедуры, не привязываясь к конкретной реализации.
module integrator_mod
implicit none
abstract interface
pure function integrand(x) result(y)
real, intent(in) :: x
real :: y
end function integrand
end interface
contains
function integrate(f, a, b, n) result(s)
procedure(integrand) :: f ! любая функция нужной сигнатуры
real, intent(in) :: a, b
integer, intent(in) :: n
real :: s, h, x
integer :: i
h = (b - a) / n
s = 0.5 * (f(a) + f(b))
do i = 1, n - 1
x = a + i * h
s = s + f(x)
end do
s = s * h ! метод трапеций
end function integrate
end module integrator_mod
Здесь procedure(integrand) :: f объявляет, что f — это процедура с интерфейсом integrand. Любую pure-функцию real->real можно передать в integrate как аргумент, и компилятор проверит её совместимость. Это пример функций высшего порядка в Fortran — и работают они именно благодаря явным интерфейсам через abstract interface. Без знания сигнатуры компилятор не смог бы ни проверить переданную функцию, ни корректно её вызвать внутри. Тот же abstract interface лежит в основе deferred-методов абстрактных типов, которые мы разберём в разделе про ООП, и процедурных компонентов производных типов. Таким образом, явный интерфейс — это не узкая техническая деталь, а фундамент целого пласта выразительных возможностей современного Fortran: от безопасной передачи массивов до функций высшего порядка и полиморфизма.
Историческая перспектива: цена «слепых» вызовов
Чтобы по-настоящему оценить ценность явных интерфейсов, полезно вспомнить, какой ценой обходилось их отсутствие в эпоху Fortran 77. Тогда несоответствие типов аргументов между вызовом и процедурой было одним из самых коварных классов ошибок во всём программировании. Передали целое там, где ждали вещественное, — и процедура интерпретировала битовый узор целого как число с плавающей точкой, получая бессмыслицу. Перепутали порядок аргументов одного типа — и расчёт молча шёл по неверным данным. Передали массив не той длины — и процедура читала или писала за его пределами, повреждая чужую память. Самое страшное, что всё это происходило без единого предупреждения: компилятор не имел информации, чтобы заметить расхождение, а проявлялась ошибка где-то далеко от причины — иногда в виде неверного результата, иногда падением через тысячи строк после. Отладка таких дефектов в больших научных кодах отнимала дни и была настоящим бедствием, подрывающим доверие к расчётам. Явные интерфейсы устранили этот класс ошибок как явление: теперь компилятор сверяет каждый вызов с сигнатурой и отказывается компилировать несовместимый код, перенося обнаружение проблемы из мучительной рантайм-охоты в мгновенную диагностику на этапе сборки. Это один из крупнейших шагов Fortran к надёжности, и именно поэтому современная практика — держать весь код в модулях, где интерфейс явен всегда. Переход на явные интерфейсы по влиянию на качество кода сопоставим с переходом на implicit none: оба отняли у компилятора право «молча додумывать» и заставили программиста выражать намерения явно, а компилятор — их проверять.
Частые ошибки
- Использовать ассумированные массивы у внешней процедуры без блока
interface. Компилятор либо откажется, либо (в нестрогом режиме) сгенерирует неверный вызов. Лекарство — модуль или явный блокinterface. - Рассинхрон ручного
interfaceи тела. Если поменяли тело процедуры, но забыли поправить блокinterfaceу потребителя, получите неопределённое поведение. Поэтому модули предпочтительнее: там сигнатура одна. - Думать, что
interfaceопределяет процедуру. Нет, он лишь описывает сигнатуру; тело должно существовать где-то ещё, иначе линковка упадёт с «undefined reference». - Полагаться на неявный интерфейс «как раньше». Это работает лишь для простейших сигнатур и лишает вас всей современной выразительности языка.
Итоги
- Явный интерфейс — это знание полной сигнатуры процедуры в точке вызова; неявный — «слепой» вызов в стиле Fortran 77.
- Процедуры в модулях и внутренние процедуры имеют явный интерфейс автоматически — это ещё один довод за модульный стиль.
- Для внешних/чужих процедур интерфейс описывают блоком
interface(сигнатура без тела). - Ассумированные массивы, опциональные и аллоцируемые аргументы, проверка
intent— всё это требует явного интерфейса. - Ручной
interfaceопасен рассинхроном; когда возможно — кладите процедуру в модуль.