Последний раз обновлено 10.02.014
Принято, что имена, которые в контексте исходного кода С начинаются с двойного подчеркивания "__" или с подчеркивания, за которым следует заглавная буква "_X" являются системными, заведомо непортируемыми именами, поэтому имена для абстрактной машины С не могут быть такими.
Для разрешения конфликта имен мы ограниченно применяем пространства имен С++. Ограничение на применение вызвано стремлением сохранить бинарность С для объявления имени, т.е. имя может быть объявлено только в локальном или только в глобальном контексте, это ограничение не требует задействовать механизм параметризации имен функций (этот механизм стандартным компилятором не поддерживается). Практически это требование в частности означает, что локальные функции не могут объявлять локальное использование глобальных идентификаторов из произвольных пространств имен, они могут объявлять только локальные имена, определенные только в этом локальном контексте, весь остальной интерфейс функции проходит через глобальный контекст или класс его замещающий.
Напомним, что нельзя написать программу, которая оперирует POD данными неопределенной разрядности с неопределенными свойствами; поэтому в портируемой программе надо явно задавать не только наличие знака, но и такие качества POD типа данных, как:
Поэтому на замену int для абстрактной машины С мы вводим такие целые типы (классы):
uint8
Произвольное выравнивание в памяти означает, что несмотря на суффикс "8", для типа [u]intXX недопустима адресная арифметика как операции над char, все адресные операции надо проводить над "указателем на [u]intXX" (вид операции "++" в пересчете на char зависит от системы). Если под тип отведено более 8 бит, то корректное обнуление старших бит при необходимости должен выполнять компилятор/класс.
Тип предназначен для правильного выполнения арифметических операций над числами диапазона, заданного числом значащих бит, это означает, что операция 0xff + 0x01 не может завершиться правильно, поскольку результат выходит за пределы диапазона 8 бит. Этот тип работает именно с абстрактными математическими операциями, для которых не существует ни двоичного кода, ни насыщения, ни иных аппаратных сущностей процессоров.
Алгоритм, который затребовал тип [u]intXX, не только гарантирует, что на всех диапазонах входных данных проблем с переполнением возникнуть не может, но и не готов обнаруживать и обслуживать переполнения. На самом деле огромная часть реального кода использует знаковый тип int именно в таких целях, для хранения размеров, счетчиков и т.п. вещей при условии их непереполнения, к тому же еще и при их беззнаковости. На портирумых программах такие предположения часто заканчиваются плохо.
Это все происходит потому, что правильное использование типа программистом не продумывается. С этим нежеланием ничего нельзя поделать, кроме того чтобы во время выполнения контролировать правильность использования типа. Именно этим целям и служит тип [u]intXX. При возникновении знакового или беззнакового переполнения во время арифметических операций над этим типом генерируется исключение.
Особый комментарий для x86, система команд которого содержит аппаратное исключение для переполнения при выполнении команды div (деление на ноль). Это исключение даже позволяет выполнить рестарт сбойного участка кода. Вероятно в подобном поведении есть какая-то историческая причина, но мне она неизвестна. Для типа [u]intXX неудача подобной операции не отличается от неудачи в 0xff + 0x01 и означает выставление флагов CF/[OF] и вызов исключения.
Операции логические двоичные, а также сдвига и ротации бит определены также как и для регистрового типа, т.е. предполагается, что число хранится в двоичном прямом или дополнительном коде. Переполнения при сдвигах влево вызывают исключения.
Какова эффективность выполнения операций над типом [u]intXX в конкретной системе, можно ли использовать такой тип в своих портируемых программах? Этот тип арифметический, он не эмулирует точное поведение целочисленного регистра процессора, т.е. его возможности меньше чем у регистра, поэтому в конкретной системе, где есть подходящие регистры процессора, этот тип будет выполняться в них достаточно эффективно. Выполнение каждой арифметической операции контролируется, что в принципе дает код немного большего размера и немного более медленный, чем при использовании регистровых типов, конкретное ухудшение зависит от процессора. Этот тип позволяет убрать из кода явные проверки, но в коде, где генерация исключений недопустима, такой тип использовать нельзя.
ureg8
Регистровый тип [u]regXX отличается от арифметического тем, что он эмулирует поведение традиционного целочисленного регистра ALU процессора: биты могут совсем не иметь арифметической интерпретации; арифметические операции выполняются в двоичном прямом и дополнительном коде, переполнение вызывает потерю старшего значащего разряда. Библиотечными функциями типа "macs" над регистровыми типами возможна арифметика с насыщением. Определены логические битовые операции. Исключения при переполнениях (включая деление на ноль) не генерируются.
Для достаточно низкоуровневых программ этот тип данных основной, он позволяет программисту контролировать пределы и диапазоны только в нужных местах, но явным образом.
bs8
Битовый набор это тип для системно-независимого описания двоичных данных. Такие типы применяются в двоичных файлах, при передаче данных по сети и т.п.
Над типом определены операции логические двоичные, сдвига и ротации бит. Значение такого типа может быть передано в регистровый тип и наоборот.
Такой тип почти на всех конкретных системах будет работать неэффективно, поскольку битовое выравнивание никак не согласовано с размером char конкретной системы, однако применяется он довольно широко, например, при считывании заголовка двоичного файла в память одним блоком, поля заголовка будут представлены в памяти именно типом bsXX.
Кроме абсолютно системно-независимого bsXX, в абстрактной машине С для портируемой программы используется и абсолютно системно-зависимый тип char, для которого битовый размер не определен. Нет ли здесь противоречия с предыдущими утверждениями, приведшими нас к необходимости заменить int?
Противоречий нет, char нужен конечно, не для того чтобы хранить в нем коды символов (ASCII коды можно хранить в типе ureg8), а для того чтобы перебирать память без пропуска битов.
Потому для абстрактной машины С мы вводим такие целые типы char (классы):
char
Типичное применение char выглядит так "memset(&p,0,sizeof(p))". Ноль в этой функции имеет тип char, а "sizeof(p)" возвращает число chars в типе переменной "p".
Помимо использования в качестве инструмента для доступа к памяти, над типом char можно выполнять логические и арифметические операции также как над регистровым типом, но требуется помнить, что битовый размер типа char произвольный, при этом с одной стороны для типа char нет контроля переполнения, как для типа intXX, с другой стороны, для типа char нет эмуляции аппаратного регистра, как для типа regXX (эмуляция есть, но неизвестно на каком бите). В арифметико-логическом смысле тип char объединяет в себе худшие качества арифметического и регистрового унифицированного типа.
Итак, программа которая использует только унифицированные типы данных абстрактной машины С, на всех конкретных машинах будет работать совершенно правильно и не потребует специальной перепроверки алгоритма. Это очень большое достижение, значит не зря столько лет развивались компьютерные технологии.
Старый код С, который не использует унифицированные типы, не может быть перенесен на абстрактную машину С без переделок.
А теперь подумаем, могут ли современные стандартные компиляторы позволить нам использовать унифицированные типы данных? Для работы с этими типами нам понадобится или нестандартный компилятор С, который прямо понимает эти типы, или нам понадобятся классы С++. Несмотря на все недостатки классов С++, для создания вариаций POD типов, возможностей этих классов и семантики копирования будет вполне достаточно.
А еще можно получить портируемую программу на стандартном компиляторе и совсем ничего для этого не делать. Если вы хотите написать портируемую программу, которая сразу будет выполняться на вполне конкретном окружении, то можно использовать унифицированные регистровые типы заданные через typedef, а созданием классов для этих регистровых типов на несовместимых окружениях будет заниматься тот, кто будет туда портировать вашу программу. Примеры задания регистровых типов через typedef.
namespace Nacm16{}
//для 16 битного окружения x86
typedef unsigned char ureg8;
typedef unsigned int ureg16;
typedef unsigned long ureg32;
typedef char reg8;
typedef int reg16;
typedef long reg32;
typedef unsigned char uchar;
namespace Nacm32{}
//для 32 битного окружения x86
typedef unsigned char ureg8;
typedef unsigned short ureg16;
typedef unsigned int ureg32;
typedef unsigned long ureg32;
typedef char reg8;
typedef short reg16;
typedef int reg32;
typedef unsigned char uchar;
Как видим, программа пользующаяся этими унифицированными типами сможет работать на 16 и 32 битном окружении. Если программист необоснованно не запрашивает данные размером 32 бита, то алгоритмы, оригинально написанные для исполнения на 32 битном окружении, но способные отработать на 16 битном окружении, будут работать на 16 битном окружении также эффективно.
Казалось бы, сегодня 16 битное окружение можно найти разве что в некоторых мобильных устройствах, периферийных устройствах и контроллерах, но принцип, лежащий в основе несовместимости между 16 и 32 битами, точно такой же, какой лежит в основе несовместимости между 32 и 64 битами. Все, что сказано для границы 16 бит остается абсолютно справедливо и для границы в 32 бита и для любой другой границы.