Аргументы и 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.
Проверьте себя
1. Как Fortran по умолчанию передаёт аргументы в процедуры?
AПо значению (копия)
BПо ссылке: процедура работает с оригиналом переменной
CТолько глобально
DЧерез возвращаемое значение
2. Что произойдёт при попытке изменить аргумент с атрибутом intent(in)?
AИзменится копия
BОшибка компиляции
CИзменится оригинал
DНичего, запись игнорируется
3. Почему опасно читать аргумент intent(out) до того, как в него что-то записали?
AЭто всегда даёт ноль
BНа входе он считается неопределённым (там может быть мусор)
CЭто запрещено синтаксисом
DОн равен значению вызывающего