«Нежданчики» языка Фортран

в 6:08, , рубрики: fortran, Блог компании Intel, Компиляторы, Программирование
«Нежданчики» языка Фортран - 1

Многие из нас, обучаясь программированию ещё в университетах или дома, делали это на языках С/С++. Конечно, всё зависит от времени, в которое начиналось наше знакомство с языками программирования. Скажем, кто-то начинал с Фортрана, другие — с Basic’a или Delphi, но стоит признать, что доля начавших свой тернистый путь программиста с С/С++ наибольшая. К чему я всё это? Когда перед нами стоит задача изучить новый язык и написать на нём код, мы часто основываемся на том, как бы я это написал на своём «базовом» языке. Сузим вопрос — если нужно написать что-то на Фортране, то мы вспоминаем, как бы это было реализовано на С и делаем по аналогии. Очередной раз столкнувшись с тонкостью языка, которая привела к абсолютно неработающему алгоритму и большой проблеме, эскалированной мне, я решил отыскать как можно больше нюансов языка Фортран, по сравнению с С, с которыми столкнулся лично. Это своего рода «нежданчики», которые ты явно не планировал увидеть, а они бац – и всплыли!
Конечно, речь не пойдёт о синтаксисе — в каждом языке он свой. Я попробую рассказать о глобальных вещах, способных изменить всё «с ног на голову». Поехали!

Передача аргументов в функции
Все мы помним, что таким кодом на С изменить значение переменной a в вызывающей main функции нельзя:

void modify_a(int a)
{
  a = 6;
}

int main()
{
  int a = 5;
  modify_a(a);
  return 0;
}

Всё правильно – аргументы в функцию в языке С передаются по значению, таким образом изменить a в функции modify_a не получится. Для этого нужно передать аргумент по ссылке и тогда мы будет работать с той самой a, переданной из вызываемой функции.
Так вот, «нежданчик» номер «раз» заключается в том, что в Фортране всё наоборот! Аргументы передаются в функции по ссылке, и подобный код вполне будет изменять значение a:

a = 5
call modify_a (a)
contains
subroutine modify_a (a)
  integer a
  a = 6
 end subroutine modify_a
 end

Думаю, что все понимают проблемы, которые могут появиться от незнания данного факта. Причем проявляться эта специфика может много где, в частности, при работе с указателями, но об этом будет отдельный разговор.

Работа с массивами
По дефолту, индексация массивов в Фортране начинается с 1, а не с 0, как в С. То есть real a(10) дает нам массив от 1 до 10, а в С float a[10] идет от 0 до 9. Тем не менее, мы можем задать массив и как real a(0:100) в Фортране.

Кроме того, многомерные массивы хранятся в памяти в Фортране по столбцам. Таким образом обычная матрица

«Нежданчики» языка Фортран - 2

располагается в памяти так:

«Нежданчики» языка Фортран - 3

Не забываем об этом при работе с массивами, особенно, если передаем их в/из функции на С через библиотеки.

Необъявленные переменные
Фортран по умолчанию не будет ругаться на данные, которые мы не объявили явно, потому как здесь есть понятие неявных типов данных. Пошло это с стародавних времён, и идея заключается в том, что мы сразу можем работать с данными, а тип у них будет определяться в зависимости от первой буквы в имени – во как хитро!
Попытка собрать код с компилятором С предсказуемо выдаст ошибку ‘b: undeclared identifier’:

int main()
{
	b = 5;
}

В Фортране сработает на ура:

i = 5
end

Сколько же абсолютно разноплановых ошибок в коде может быть от этого. Поэтому, не забываем добавлять в код IMPLICIT NONE, запрещающее подобные «игры» с неявными объявлениями:

implicit none
i = 5
end

И сразу видим ошибку: error #6404: This name does not have a type, and must have an explicit type. [I]
Кстати, язык Фортран не требователен к регистру, поэтому переменные a и A – это одно и то же. Но это уже синтаксис, о котором я обещался не говорить.

Инициализация локальных переменных
Казалось бы, чем подобная инициализация может быть плоха:

real :: a = 0.0

И чем она отличается от такой:

real a
a = 0.0

Неожиданный сюрприз для разработчиков на С – в Фортране есть принципиальное различие в этом! Если локальная переменная инициализируется в момент декларации, то к ней неявно применяется атрибут SAVE. Что это за атрибут? Если переменная объявлена как SAVE (явно или неявно), то она является статической, а значит инициализируется только при первом заходе в функцию. При последующих входах в функцию сохраняется предыдущее значение. И это может быть совсем не тем, что мы ожидаем. Как совет – избегать подобных инициализаций, и при необходимости использовать атрибут SAVE явно. Кстати, у компилятора даже есть отдельная опция -save, позволяющая менять настройки по умолчанию (выделение на стэке) и делать все переменные статическими (кроме случаев рекурсивных функций и тех переменных, которые явно объявлены как AUTOMATIC).

Указатели
Да, в Фортране тоже есть понятие указателей. Но используются они гораздо реже, потому что выделять память динамически в нем можно и без их помощи, а аргументы итак передаются по ссылке. Стоит отметить, что механизм указателей сам по себе работает в Фортране по-другому, поэтому остановлюсь подробней на этом.
Здесь нельзя сделать указатель на любой объект – только на тот, который объявлен специальным образом. Например, так:

real, target :: a
real, pointer :: pa
pa => a

С помощью оператора => мы ассоциируем указатель pa с объектом a. Не стоит пытаться выполнить операцию присваивания вместо =>. Всё успешно соберётся, но упадёт в рантайме. Так что тем, кто привык просто присваивать указатели в С придётся заставлять писать каждый раз => вместо =. Сначала забываешь, но потом втягиваешься.
Если хотим, чтобы указатель не был ассоциирован с объектом, используем nullify(pa) – это своего рода и инициализация указателя. Когда мы просто объявляем указатель, его статус в Фортране неопределен, и функция, проверяющая его ассоциацию с объектами (associated(pa)) будет работать некорректно.
Кстати, почему нельзя ассоциировать указатель с любой переменной того же типа, как это делается в С? Во-первых, так захотелось в комитете по стандартизации. Шучу. Скорее всего, всё дело в ещё одном уровне защиты от потенциальных ошибок – просто так мы теперь точно не сможем связать указатель со случайной переменной, ну и подобное ограничение дает компилятору больше информации, а, следовательно, больше возможностей для оптимизации кода.
Кроме того, что тип указателя и объекта должны совпадать, а сам объект должен быть объявлен с атрибутом TARGET, есть ещё ограничение и на размерность массивов. Скажем, если мы работаем с одномерными массивами, то и указатель должен быть объявлен соответствующим образом:

real, target :: b(1000)
real, pointer :: pb(:)

Если бы массив был двумерный, то указатель бы был pb(:,:). Естественно, что размер массива в указателе не задается – мы же не знаем, с каким массивом будет ассоциирован указатель. Думаю, логика понятна. После ассоциации, мы можем работать с указателем как обычно:

b(i) = pa*b(i+1)

Что то же самое, что написать b(i) = a*b(i+1). Можно и значение присвоить, например pa = 1.2345.
Таким образом, значение у a будет 1.2345. Интересная особенность указателей Фортрана заключается в том, что с их помощью можно работать с частью массива.
Если мы написали b => pb, то можем работать с 1000 элементами массива b через указатель pb.
Но можно написать и так:

pb => b(201:300)

В этом случае мы будем работать с массивом только из 100 элементов, а pb(1) – это b(201).
Забавно, как можно использовать функцию выделения памяти allocate в случае указателей. Написав allocate(pb(20)) мы выделим дополнительно 20 элементов массива типа real, которые будут доступны только через указатель pb.
Вообщем, человеку привыкшему к С, всё это будет казаться необычным. Но, если начать писать код, то достаточно быстро привыкаешь, и всё начинает казаться удобным.
Разработчик, натолкнувший меня на идею написания этого блога, тоже так думал и активно работал с указателями направо и налево, создавая код, алгоритм которого использует дерево, но не учитывал одну особенность. На Фортран переписывался этот Сишный код:

void rotate_left(rbtree t, node n) 
{
  node r = n->right;
...

У структуры node есть поля, содержащие указатели node*, например right.
В функции создается локальная переменная r, ей присваивается значение n->right и так далее и тому подобное. Реализация на Фортране получилась такой:

subroutine rotate_left(t, n)
type(rbtree_t) :: t
type(rbtree_node_t), pointer :: n
type(rbtree_node_t), pointer :: r
r => n%right
...

И вот тут, в самом начале, кроется «ошибка ошибок». Мы ассоциировали указатель r с n%right. Изменяя в дальнейшем коде r, мы будем менять и n%right, в отличие от С, где будет изменяться только локальная переменная r. В итоге, всё дерево превратилось непонятно во что. Выход из ситуации — ещё один локальный указатель:

subroutine rotate_left(t, n_arg)
type(rbtree_t) :: t
type(rbtree_node_t), pointer :: n_arg
type(rbtree_node_t), pointer :: r
type(rbtree_node_t), pointer :: n
        
n => n_arg
r => n%right
...

В этом случае, если мы в дальнейшем меняем ассоциацию у указателя n, то это никак не затронет «внешний» n_arg.

Стринги
Ну и напоследок, одна маленькая особенность, попортившая огромное количество памяти в mixed приложениях (С и Фортран). Как вы думаете, в чем может быть разница при работе с стрингами в С:

char string[80]="test";

И Фортране:

character(len=80) :: string
string = "test"

Ответ легко поможет дать отладчик. В этом случае, в Фортране оставшиеся неиспользованными байты забиваются пробелами. При этом нет типичного для С символа окончания строки /0, поэтому нужно быть предельно аккуратным, передавая стринги из Фортрана в С и обратно. Опять скажу, что для того, чтобы безопасно работать с С и Фортраном, нужно использовать специальный модуль ISO_C_BINDING, который разрешает и данное различие, и много других проблем.

На этом заканчиваю свой рассказ. Теперь вы точно знаете самые важные различия между С и Фортраном, и если уж придётся написать код на последнем, я думаю, сделаете это не хуже, чем на С, правда? Ну а данный пост будет в помощь.

Автор: ivorobts

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js