Лекции (1129116), страница 20
Текст из файла (страница 20)
Понятно, что инкапсуляция происходит за счет технологической дисциплины. Например, использование extern в программе – плохой стиль. Так как это означает, что программист сам описывает контекст и может ошибиться.
Но тут возникает интересная ситуация. Поскольку раздельная трансляция не удовлетворяла Страуструпа, но ему нужно было обеспечить совместимость с языком С, то он применил достаточно хитрый способ, который позволяет некоторые ошибки увидеть до начала выполнения программы. Прием заключается в том, что для того, чтобы можно было использовать стандартные загрузчики и соблюдать эквивалентность между объявленным и реальным контекстом, в компиляторе C++ генерируется некоторое новое имя функции и код, такие что в нем зашифрован весь профиль функции: типы возвращаемых значений, параметров, их количество.
Таким образом при сборке различных модулей, если в одном из них экспорт функции объявлен неверно, то загрузчик выдаст сообщение об ошибке. И мы можем использовать старые загрузчики (не самые, конечно, древние), не меняя систему программирования.
Вобщем, можно понять, что независимая трансляция – не очень хорошая вещь.
Раздельная зависимая трансляция
Надо понимать, что при зависимой трансляции КТ извлекается не из самого модуля, а из специальной библиотеки. Пусть у нас есть некая единица трансляции (некий текст, который дается на вход компилятору), так как трансляция у нас зависимая, то компилятор должен еще использовать некий контекст – совокупность всех единиц компиляции образует программную библиотеку.
Трансляционная библиотека – это некая библиотека, содержащая все контексты трансляционных единиц.. Поэтому конструкции, которые определяют контекст, указывают, что мы должны взять из трансляционной библиотеке.
Когда мы говорили о типах, то упоминали, что во многих языках понятия физического и логического модулей совпадают. Причем модули делятся на реализационные и модули определений. Совершенно очевидно, что трансляционная библиотека будет состоять из модулей определений, так как только они и нужны, чтобы обработать текст. Причем как хранится ТБ (на диске, в памяти, где-то еще) – неважно.
Modula-2
С этой точки зрения в Modula-2 самая простая ситуация: у нас есть ТБ, которая состоит из всех модулей определений. Транслятор использует только модули определений. Поэтому никаких проблем со скрытыми типами нет, но только в случае, если АТД пресдставлен указателем. Прежде всего, это из-за простоты. С другой стороны можно обеспечить все контроли межмодульных интерфейсов. В единице компиляции у нас может быть import (это и есть указание контекста), либо:
IMPORT M;
либо
FROM M IMPORT …;
Oberon
В Oberon все еще проще. В нем все – один модуль, вся программная библиотека – библиотека модулей:
MODULE M;
…
END;
Возникает вопрос – что хранится в трансляционной библиотеке? В ней хранятся все имена (для каждого модуля заведен соответствующий раздел), помеченные знаком “*” или “*-“ (последние – это переменные только для чтения).
Больше нагрузки ложится на транслятор. Например, в Modula-2, так как были разделены уже заранее, на уровне исходных текстов, модули определений и соответствующие модули реализаций, то это позволяло при трансляции нужна была только трансляционная библиотека, и мы не должны были транслировать все сразу. То есть минимизируется количество перекомпиляций, что во время создания языка было достаточно критическим параметром.
Для достижения аналогичных результатов в языках с независимой компиляцией (например, С) вводится ряд внешних утилит. Так в Unix язык C сразу «оброс» такими программами, как make, lint. Первая занимается как раз определением зависимостей между исходными текстами, файлами заголовков, объектными модулями и, собственно, запускаемым файлом, данные зависимости определяются по временным отметкам файлам. Вторая (lint) – это верификатор межмодульных связей, которому на вход дается весь проект, и поскольку ему (в отличие от стандартного компилятора) теперь доступны все файлы, он проверяет ошибки межмодульных связей, то есть он делает то, что должен был бы делать компилятор С, если бы делал раздельную зависимую трансляцию. Понятно, что все эти утилиты вместе образуют некоторую среду, позволяющую как-то избавляться от проблем.
На сегодняшний день среды разработки на C++ включает транслятор, утилиту make и верификатор связей, подобный lint. Например, и BC++ и MSVC всегда генерируют makefile.
Возвращаясь к языку Oberon следует сказать, что на первый взгляд все преимущества, связанные с минимизацией времени трансляции кажутся потерянными, поскольку у нас в явном виде трансляционная библиотека не определяется, а определяется она самим транслятором. Что будет, если изменить какой-то модуль? Будут перетранслированы все клиентские модули. Получается, что преимущества разделения на модули определений и реализации, которые имеются хотя бы в Modula-2 потеряны. На самом же деле это не есть недостаток языка. Это недостаток транслятора.
Опять вернемся к языку С. Заметим, что если в header файле добавить пробел или удалить комментарий, то будут перетранслированы все клиентские модули, хотя с разумной точки зрения это бессмысленно. А вот в Turbo Pascal, где реализована фактически обероновская схема (модуль реализации и определения объединены в одном), подобного не произойдет. Будет на всякий случай перекомпилирован сам модуль, но клиентские части останутся прежнеими Почему? Дело в том, что оттранслированный unit в Turbo Pascal в начале состоит из и определений, а затем из части реализации. Поэтому транслятор при перекомпиляции смотрит – насколько изменилось определение – при добавлении, например, пробела сгенерированный двоичный код никак не изменяется. Также делает Oberon.
Таким образом, соединение воедино частей реализации и определений в одном модуле неприятных последствий не оказывает.
В С/С++ есть механизм предкомпилированных header’ов, который почти повторяет эту схему, но сам механизм гораздо более сложный, чем чистая реализация в Turbo Pascal или Oberon.
Ada
В Ada, когда мы говорили об определении новых типов, то видели модули двух видов: спецификация пакета и тело пакета:
package P is
…
end P;
package body P is
…
end P;
Очевидно, что они играют ту же роль, что и модули определений/реализации в Modula-2. Причем в отличие от Modula-2 эти две конструкции могут быть, как объединены физически, так и разделены. Более того, даже одна процедура может быть единицей компиляции. И это еще не все.
Мы говорили, что недостатком линейной схемы организации модулей, когда все модули равноправны, явлется неподдержка сформировавшейся методологии программирования, такая линейка модулей поддерживает программирование «снизу вверх», а есть еще и «сверху вниз». В этом плане зависимости между модулями в Modula-2 – односторонии. Модуль М не знает, куда его экспортируют, с другой стороны есть клиентские модули, которые явно говорят, что они импортируют:
IMPORT M;
В Ada есть примеры более сложной связи, а именно, двухсторонней связи. Это связано с тем, что раздельная трансляция поддерживается в Ada на всех уровнях. Мы уже говорили, что в Ada могут быть вложенные пакеты. Внутри одного пакета может быть описан другой:
package P is
…
package P1 is
…
end P1;
…
end P;
package body P is
…
package body P1 is
…
end P1;
…
end P;
Такая схема поддерживает программирования поддерживает схему «сверху вниз»: пакет P1 имеет смысл только в контексте модуля P (в схеме «снизу вверх» P1 имеет смысл сам по себе). Получается, если мы не разрешим раздельную трансляцию P1, то тело P может сильно разбухнуть. Возникает вопрос – как это сделать, ведь P1 локально внутри P? В Ada были введены специальные конструкции. Естественно, что интерес представляет раздельная компиляция тела вложенного пакета. Мы пишем следующим образом:
package P is
…
package P1 is
…
end P1;
…
end P;
А тело пакета оформляется следующим образом, в нем появляется заглушка для тела P1:
package body P is
…
package body P1 is separate;
…
end P;
Ключевое слово separate говорит, что тело P1 будет транслироваться отдельно. Где же будет находится это тело?
separate (P);
package body P1 is
…
end P1;
В начале мы указываем, что P1 вложен в P (ведь у нас может быть несколько пакетов с именем P1 на различных уровнях)
В результате мы имеем пример двусторонней, более сложной связи. Причем, можно таким образом поступать и с телами процедур.
Пока мы еще ничего не сказали о том, как в Ada указывается контекст трансляции (КТ). Это очень просто – раздельно транслируемый модуль отличается от обычного, находящегося в файле лишь наличием в начале определения WITH <список имен>. Кроме того, может быть добавлена конструкция USE <список имен>.
WITH делает объекты непосредственно видимыми в данном модуле. Если есть:
package P is
TYPE T IS ..
X: T;
end P;
и другая единица компиляции:
with P; use P;
package Q is
Y: P.T;
end Q;
package body Q is
Y:=P.X;
end Q;
В результате P видимо непосредственно, а все объекты внутри P – потенциально.
Для чего нужна конструкция USE? Используя ее мы можем писать, а можем не писать уточнение “P.”, то есть она работает, как и вообще USE в теле пакета.
Можно разделить процедуры:
package body P1 is
procedure P2 is separate;
end P1;
и в другом физическом модуле:
separate (P1) procedure P2 is
…
end P2;
То есть мы можем свободно разделять физические единицы компиляции. Правила Ada настолько гибки, что позволяют не видеть разници между раздельной и цельной компиляциями. Мы можем слить все в один файл, а можем, используя указатели контекста, сделать разбиение на несколько файлов. Но эффект трансляции не изменится.
Заметьте, что в Modula-2 и Oberon совсем не так – если пишется библиотечный модуль, то мы обязаны транслировать его отдельно. Зачем сделано по-другому в Ada? Для гибкости и удобства программиста.
Но за все надо платить. В Ada приходится платить приватной частью спецификаций, а именно, то, что сделано в Oberon и Modula-2 (если изменяется реализация, то клиентские модули не перетранслируются), в Ada не сделано. Поскольку спецификация пакета выглядит следующим образом:
package P is
type T is private;
private
type T is record … end record;
end P;
Понятно, что это есть реализация, здесь нет никаких текстов процедур, поскольку инкапсулированный код находится в теле пакета P. И изменение тел процедур (приватных) для типа T – оно инкапсулировано. Если тело P транслируется отдельно, то никакого изменения в клиентских модулях не требуется, их перетранслировать не нужно.
Если же мы меняем структуру типа T, то следует перетранслировать все клиентские части. Это – недосаток. Конечно, с современной точки зрения это несущественно.
C++
Заметьте, что на первый взгляд классовые языки, такие как C++ обладают такими же недостатками. Если есть:
class X{
public: // если абстрактный тип данных, то эта часть состоит
//только из функций
…
private: // эта часть состоит из структур данных
// и приватных функций
…
};
Следует подчеркнуть, что публичная часть должна состоять только из функций в АТД, но не обязательно в абстрактном классе.
Получается, что любое изменение приватной части приводит к перекомпиляции всех клиентских модулей, несмотря на то, что интерфейс не изменился. На первый взгляд, это кажется не очень большим недостатком, однако, учитывая независимую трансляцию модулей в C++ это иногда приводит к большим накладным расходам, ибо большинство компилируемых единиц приходится не на написанный код, а на используемые библиотеки (win32Api, Motif и т.д.). Иногда ¾ и более компилируемого текста приходится на эти библиотеки. Опять же современные средства программирования на C++ содержат концепцию прекомпилированных header’ов, то есть эти заголовки хранятся в специальном формате, и компилятор перед обработкой текста пытается что-то взять сначала оттуда. Однако, программисты, как на MSVC, так и на BC++ знают, что это уменьшает время трансляции и добавляет немного головной боли, ибо сам язык такой концепции не поддерживает.
Страуструп предложил использовать концепции абстрактных интерфейсов, абстрактного класса, виртуальных методов и наследования для минимизации времени компиляции. Очень интересно, что понятия наследования и динамического связывания позволяют решать проблемы, которые на первый взгляд к ООП не относятся. Но об этом мы будем говорить позже.
Java
В Java безусловно трансляция раздельная и зависимая. Еще хорошо то, что когда мы указываем контекст, мы всегда указываем имя некоторого логического модуля, который одновременно является и единицей компиляции и должен содержаться в каком-то файле. Логический модуль в Java – это класс. Есть файл (единица компиляции). Есть физический модуль (пакет), он состоит из ряда файлов. Больше в Java ничего нет – там нет ни глобальных переменных, ни глобальных функций.