Аргументы и intent
Атрибут intent — это контракт: он объявляет, читает процедура аргумент, пишет в него или делает и то, и другое.
intent — атрибут аргумента процедуры, объявляющий направление передачи данных:
intent(in)— только чтение,intent(out)— только запись (результат),intent(inout)— чтение и запись.
В Fortran аргументы передаются особым образом, и от понимания этого зависит корректность каждой процедуры. Ключ к ясному и безопасному коду — атрибут intent, которым вы явно объявляете роль каждого аргумента. Это не просто документация: компилятор использует intent для проверок и оптимизаций. Этот урок разбирает три вида намерения, объясняет лежащую в основе передачу по ссылке и показывает, как intent защищает от целого класса ошибок. Без грамотного intent процедуры в Fortran писать нельзя.
Передача по ссылке
Принципиальный факт: Fortran по умолчанию передаёт аргументы по ссылке (by reference) — процедура получает не копию, а доступ к самой переменной вызывающего кода. Поэтому изменение аргумента внутри процедуры меняет оригинал. Именно так работала субрутина swap из прошлого урока.
program by_reference
implicit none
integer :: n
n = 10
call double_it(n) ! n передаётся по ссылке
print *, "n после вызова:", n ! изменилось!
contains
subroutine double_it(x)
integer, intent(inout) :: x
x = x * 2 ! меняем оригинал вызывающего
end subroutine double_it
end program by_reference
Вывод:
n после вызова: 20
Переменная n вышла из вызова изменённой, потому что double_it работала с самой n, а не с её копией. Это отличает Fortran от языков с передачей по значению (как C для скаляров), где функция получает копию и оригинал не меняется. Контроль этого поведения — задача атрибута intent.
Три вида intent
Каждый аргумент процедуры стоит снабжать атрибутом intent, объявляя его роль. Это делает контракт процедуры явным и включает проверки компилятора.
| Атрибут | Смысл | Можно читать? | Можно писать? |
intent(in) | входные данные | да | нет |
intent(out) | результат | нет (до записи) | да |
intent(inout) | вход и выход | да | да |
program intent_demo
implicit none
real :: a, b, s, d
a = 7.0
b = 3.0
call sum_diff(a, b, s, d)
print *, "Сумма:", s, " Разность:", d
contains
subroutine sum_diff(x, y, total, delta)
real, intent(in) :: x, y ! только читаем
real, intent(out) :: total, delta ! только пишем (результаты)
total = x + y
delta = x - y
end subroutine sum_diff
end program intent_demo
Вывод:
Сумма: 10.0000000 Разность: 4.00000000
Здесь x и y — входные (intent(in)), и попытка изменить их внутри процедуры вызовет ошибку компиляции — защита от случайной порчи входа. total и delta — выходные (intent(out)): через них процедура возвращает несколько результатов, что субрутины делают элегантно, а функции — нет.
intent(in) как защита
Главная польза intent(in) — он превращает аргумент в «только для чтения». Любая попытка записи в него — ошибка компиляции, а не тихий баг. Это бесценно: вы декларируете «я не порчу этот вход», и компилятор это гарантирует.
program protect_input
implicit none
real :: v(3)
v = [1.0, 2.0, 3.0]
print *, "Норма:", vector_norm(v)
print *, "Вектор не тронут:", v
contains
function vector_norm(vec) result(n)
real, intent(in) :: vec(:) ! не изменяем входной массив
real :: n
n = sqrt(sum(vec**2))
! vec(1) = 0.0 ! раскомментируй -> ошибка компиляции
end function vector_norm
end program protect_input
Вывод:
Норма: 3.74165750 Вектор не тронут: 1.00000000 2.00000000 3.00000000
Объявив vec как intent(in), мы гарантировали неизменность входного массива. Это не только ловит ошибки, но и служит документацией: читателю сразу ясно, что функция лишь читает данные. Хорошее правило: помечайте intent(in) всё, что не должно меняться — это большинство аргументов.
Осторожно с intent(out)
У intent(out) есть коварная тонкость: при входе в процедуру такой аргумент считается неопределённым (его прежнее значение «забывается»). Поэтому нельзя читать intent(out)-аргумент до того, как вы сами в него что-то записали — там может быть мусор. Если нужно и прочитать старое значение, и записать новое, используйте intent(inout).
program out_caution
implicit none
integer :: counter
counter = 100
call reset_and_report(counter)
print *, "Счётчик:", counter
contains
subroutine reset_and_report(c)
integer, intent(out) :: c
! print *, c ! ОПАСНО: c здесь неопределён, не 100!
c = 0 ! сначала записать
print *, "Сброшен на", c
end subroutine reset_and_report
end program out_caution
Вывод:
Сброшен на 0 Счётчик: 0
Передача по ссылке против передачи по значению в C
Стоит подробнее понять, чем модель Fortran отличается от той, к которой привыкают в C, потому что именно здесь рождается большинство недоразумений у новичков. В C простые переменные (скаляры) передаются по значению: функция получает собственную копию, и сколько бы она ни меняла свой параметр, оригинал вызывающего остаётся нетронутым. Чтобы в C изменить переменную вызывающего, программист обязан явно передать на неё указатель и работать через разыменование. То есть изменчивость аргумента в C — это осознанный выбор, видимый прямо в сигнатуре по звёздочке указателя.
В Fortran всё наоборот: передача по ссылке — это поведение по умолчанию для всего, и скаляров, и массивов. Никакого синтаксиса «взять адрес» писать не нужно — он подразумевается. Поэтому процедура потенциально может изменить любой свой аргумент, и единственное, что отделяет «безопасный» аргумент от «изменяемого», — это атрибут intent. Можно сказать, что intent(in) в Fortran играет роль, которую в C играет const у параметра: он сообщает компилятору и читателю, что аргумент трогать нельзя. Но если в C забыть const — это лишь упущенная подсказка, то в Fortran забыть intent означает оставить дверь открытой для случайной записи в чужую переменную, которую никто не заметит.
Из этого вытекает практический вывод, важный для каждого, кто переходит на Fortran с C-подобных языков: не ждите, что передача аргумента «защитит» оригинал. По умолчанию защиты нет. Защиту вы создаёте сами, явно помечая аргументы intent(in). Привычка снабжать intent абсолютно каждый аргумент — это не педантизм, а прямой аналог дисциплины const-корректности, только в Fortran она ещё важнее, потому что молчаливое умолчание здесь опаснее.
intent как помощник оптимизатора
Атрибут intent — это не только проверка и документация; он напрямую развязывает руки оптимизатору, и понимание этого помогает писать быстрый код. Когда компилятор видит intent(in), он получает обещание: за время работы процедуры этот аргумент не изменится через данное имя. Благодаря этому значение можно один раз загрузить в регистр и пользоваться им, не перечитывая из памяти, — а перечитывание из памяти на порядки дороже обращения к регистру. Без такого обещания компилятор обязан осторожничать: вдруг где-то аргумент поменялся, и кэшированное в регистре значение устарело.
Атрибут intent(out) несёт иную, но тоже ценную информацию: входное значение аргумента не используется, оно будет полностью перезаписано. Это позволяет компилятору не тратить силы на сохранение или передачу прежнего содержимого — он вправе считать аргумент «чистым листом» на входе. Отсюда, кстати, и строгое правило не читать intent(out) до записи: компилятор имеет полное право не класть туда осмысленного значения. А intent(inout) — самый «тяжёлый» для оптимизатора случай: и читается, и пишется, поэтому никаких вольностей он не допускает. Вот почему хороший стиль — использовать максимально узкий intent: не ставьте inout там, где достаточно in, иначе вы без нужды связываете руки оптимизатору.
Подробнее о copy-in/copy-out
Выше упоминался механизм copy-in/copy-out — разберём, когда он включается и почему о нём важно знать. В идеале передача массива по ссылке означает, что процедура работает прямо с памятью вызывающего, без копирования. Но это возможно лишь тогда, когда переданный участок массива непрерывен в памяти. Если же вы передаёте нерегулярную секцию — например, каждый второй элемент (a(1:100:2)) или столбец матрицы, лежащий с разрывами, — а процедура ожидает обычный непрерывный массив, компилятор оказывается перед выбором.
Он поступает так: на входе собирает разбросанные элементы во временный непрерывный буфер (copy-in), передаёт процедуре этот буфер, а на выходе раскладывает изменённые значения обратно по исходным позициям (copy-out). С точки зрения семантики всё корректно — снаружи поведение неотличимо от честной передачи по ссылке. Но платой становятся два дополнительных прохода по данным и временная память под буфер. В горячем цикле, вызываемом миллионы раз, это может неожиданно стать узким местом. Избежать копирования помогают предполагаемо-формованные аргументы (intent(in) :: vec(:)), которые умеют принимать непрерывные срезы без буферизации, и внимательность к тому, какие именно секции массива вы передаёте. Знание этого механизма отличает того, кто просто «пишет на Fortran», от того, кто понимает, во что обходится каждая строчка с массивами.
Как работает под капотом
Почему передача по ссылке, и как intent влияет на код? Передавая аргумент, Fortran кладёт в кадр процедуры адрес переменной вызывающего, а не её значение; внутри обращение к аргументу — это разыменование этого адреса. Поэтому запись «достаёт» до оригинала. Эта модель эффективна для больших массивов: не нужно копировать мегабайты, передаётся лишь указатель и дескриптор. Атрибут intent компилятор использует двояко. Для проверок: запись в intent(in) запрещается на этапе компиляции. Для оптимизации: зная, что intent(in)-аргумент не изменится, компилятор может держать его в регистре и не перечитывать. А intent(out) сообщает, что входное значение не нужно, — компилятор вправе не передавать его и считать неопределённым на входе, отсюда и правило «сначала запиши». Заметьте: иногда (для нерегулярных секций) компилятор всё же делает копирование на входе и выходе (copy-in/copy-out), чтобы внутри процедуры массив был непрерывным; это согласуется с семантикой intent, но стоит памяти.
Частые ошибки
- Пропуск
intent. Без него теряются проверки и часть оптимизаций; снабжайтеintentкаждый аргумент. - Чтение
intent(out)до записи. На входе он неопределён; нужно сначала присвоить. Для чтения старого значения беритеintent(inout). - Ожидание копии аргумента. Fortran передаёт по ссылке: изменение аргумента меняет оригинал (если не
intent(in)). - Изменение
intent(in). Запись в него — ошибка компиляции; это и есть его защитная функция. - Скрытое copy-in/copy-out. Передача нерегулярной секции массива может вызвать копирование — учитывайте в горячем коде.
Итоги
- Fortran передаёт аргументы по ссылке: процедура работает с оригиналом, а не с копией.
intent(in)— только чтение (защита входа),intent(out)— только результат,intent(inout)— и то и другое.- Запись в
intent(in)-аргумент — ошибка компиляции; это документирует и защищает. intent(out)-аргумент на входе неопределён — сначала запишите, потом читайте.- Несколько результатов удобно возвращать через
intent(out)-аргументы субрутины. - Передача по ссылке эффективна для больших массивов, но возможны скрытые copy-in/copy-out.