Когда размер имеет значение

Это перевод довольно-таки старенькой статьи Why size_t matters взятой здесь:

http://www.embedded.com/electronics-blogs/programming-pointers/4026076/Why-size-t-matters

Однако, практика показывает, что в мире программирования (на языке С/С++) нет-нет да возникают вопросы, связанные с типом size_t. А раз так, было было бы не плохо освежить эту тему.

Использование size_t соответствующим образом может улучшить портируемость, эффективность или читабельность вашего кода. А может быть даже и все три показателя сразу.

Множество функций в Стандартной Библиотеке Си принимают или возвращают значения, которые представляют размеры объектов в байтах. Например, аргумент n в функции malloc(n) назначает размер памяти, которая будет выделена для размещения объекта. Точно так же последний аргумент в функции memcpy(s1, s2, n) сообщает размер объекта, который будет скопирован. Еще пример — возвращаемое значение функции strlen(s) представляет собой длину (число символов за исключением последнего, нулевого символа) в символьном массиве s.

Разумеется, вы могли бы ожидать, что параметры функций и возвращаемые значения представляют собой значения, которые должны быть объявлены как тип int (возможно — long, а может быть даже и и беззнаковые — unsigned int или unsigned long). На саммом деле, они — другие. Согласно стандарту, объявление для функции malloc находится файле stdlib.h и выглядит так:

void *malloc(size_t n);

Соответственно, определения функций memcpy и strlen находятся в файле string.h и выглядят так:

void *memcpy(void *s1, void const *s2, size_t n);
size_t strlen(char const *s);

Кроме того, тип size_t неоднократно фигурирует в Стандартной Библиотеке С++. Более того, библиотека С++ использует подобный символ size_type, и использует возможно даже более часто, чем size_t.

Исходя из своего опыта скажу, что большинство С и С++ программистов знают, что стандартные библиотеки используют тип size_t. Но они не знают, что представляет собой size_t, и не знают почему и для чего библиотеки используют именно. Таким образом, программисты слабо представляют как вообще нужно использовать тип size_t.

В этой статье я постараюсь объяснить, почему существует size_t и у как вам следует использовать его в своих программах.

Проблема потрабельности программ

В классическом С (тот, который описан в знаменитой книге Кернигана и Ритчи «Язык программирования С», 1978 года) не упоминается size_t.

size_t был введен в обращение Комитетом Стандартов чтобы устранить проблему портирования программ на разные компьютеры и платформы.

Давайте рассмотрим проблему написать портабельную декларацию для стандартной функции memcpy. Мы рассмотрим несколько разных деклараций и увидим как хорошо они работают, когда компилируются для разных архитектур с разными размерами адресных пространств.
Напомню, что вызов memcpy(s1, s2, n) копирует первые n байт из объекта, на который указывает s2, в объект, на который указывает s1. Функция возвращает указатель s1.  В принципе, функция может копировать объекты любого типа, Поэтому параметры функции (указатели на объекты) и возвращаемое функцией значение должны быть объявлены как «безтиповой указатель» — void *. А поскольку функция реально не изменяет объект по указателю s2,  то этот указатель должен быть объявлен как void const *. Пока проблем нет.

С другой стороны, существует серьезное опасение: как объявить третий параметр функции, который представляет размер копируемого объекта. Я подозреваю, что многие программисты для этого дела выберут простой тип int:

void *memcpy(void *s1, void const *s2, int n);

, что работает в подавляющем большинстве случаев, но ведь есть также еще много случаев, когда это не работает. Постой тип int — знаковый. Это значит, что в отдельных случаях, он может уходить в минус. Однако, размеры никогда не бывают отрицательными. Использование типа unsigned int вместо int позволит функции копировать большие объекты без дополнительных затрат.

У большей части процессоров самое большое значение unsigned int в два раза (грубо говоря) превышает положительное значение int. Например, для 16-разрядных процессоров самое большое значение unsigned int составляет 65535, а самое большое положительное значение int — 32767. Таким образом, использование unsigned int вместо int в третьем параметре функции memcpy позволит копировать большие в два раза объекты.

И хотя размерность int зависит от конкретной реализации компилятора языка С, но всегда размерность int равна размерности unsigned int для одной и той же реализации компилятора. Поэтому передача unsigned int в третьем аргументе функции тащит за собой те же самые накладные расходы, как и при передаче int.

Давайте изменим декларацию функции согласно сказанному:

void *memcpy(void *s1, void const *s2, unsigned int n);

И это превосходно работает на любых платформах, у которых беззнаковое целое может представлять максимально возможный объект в памяти. В основном это касается тех процессоров, у которых размеры регистров общего назначения (РОН) и размеры регистров-указателей памяти совпадают. Например, у MSP430 размер регистров общего назначения R4-R16 и таких регистров как Программный Счетчик (PC) и Указатель Стека (SP) — одинаковые — то есть все они 16-разрядные.

К сожалению, эта декларация натыкается на принципиальные проблемы в таких архитектурах, как, например, первое поколение Motorola 68000, у которого используются разные по размеру регистры для целых чисел и регистры  для указателей (16 бит и 32 бит, соответственно). Поэтому возникает ситуация, когда сам процессор может копировать большие объекты, но функция memcpy, таким образом, не позволяет скопировать объекты, размер которых превышает 65536 байт.

Вы говорите, это легко исправить? Просто изменить третий параметр:

void *memcpy(void *s1, void const *s2, unsigned long  n);

Теперь вы можете использовать эту декларацию для архитектур, у которых 16-разрядные РОН и 32-разрядные регистры-указатели, для копирования больших объектов. Это прием будет также работать и на полностью 16-разряжных процессорах (у которых все регистры 16-разрядные), и на полностью 32-разрядных (у которых все регистры 32-разрядные). Таким образом это обеспечит портабельность функции на разные платформы. Только есть одно «но». К сожалению, на 16-разрядных процессорах этот код будет несколько неэффективен. По понятным причинам, он будет и больше, и медленнее по сравнению с тем кодом, где в качестве третьего параметра используется unsigned int.

В стандартном С тип long (вне зависимости — знаковый или беззнаковый) должен занимать 32 бита (Смелое заявление! — прим. А.Ж.). Таким образом, 16-разрядная платформа, которая поддерживает стандартный С, на самом деле должна уметь обрабатывать 32-разрядные длинные целые. Обычно это осуществляется путем объединения двух 16-разрядных регистров в один 32-разрядный. Значит пересылка одного 32-битного длинного целого будет происходить за две машинные инструкции — по одной инструкции на каждую его часть. И действительно, почти все 32-битные операции на этих платформах требуют как минимум двух инструкций, а иногда — и даже больше.

Таким образом, декларация третьего параметра функции memcpy как unsigned long ради портируемости однозначно ухудшает эффективность на некоторых плаформах. Печалька! Хотелось бы как-то избежать этого. Так вот, использование типа size_t позволяет уйти от этих потерь.

Тип size_t — это определяемый через typedef тип. (Мягко говоря, это не совсем так. Смотри мои примечания — А.Ж.) Это — алиас (другое имя) для некоторого беззнакового целого типа, обычно unsigned int или unsigned long. Но в некоторых случая это может быть даже unsigned long long. Каждая реализация Стандартного С рассчитана на выбор беззнакового целого, которое должно быть достаточно большим (но не больше, чем это необходимо) чтобы с его помощью можно было представить размер самого большого объекта, который только может быть на этой платформе.

__________________
Мое примечание.  Какое-то время назад определение типа size_t можно было обнаружить в файле stddef.h. Я пересмотрел все свои компиляторы и кросс-компиляторы для микроконтроллеров, и не обнаружил ни этого файла, ни какого другого, где бы было написано что-нибудь типа:

typedef ... size_t;

Исходя из этого я делаю вывод, что, возможно, тип size_t является встроенным типом, и определяется конкретно при сборке самого компилятора.

Использование size_t

Определение для size_t присутствует в нескольких стандартных хэдерных файлах, а именно — в stddef.h, stdio.h, stdlib.h, string.h, time.h и wchar.h. Что касается языка С++, то там оно появляется в таких файлах, как cstddef.h, cstdio.h и так далее. Перед тем как использовать size_t, вам необходимо подключить (#include) какой-нибудь из этих хэдеров.

Подключение любого из этих файлов (в программы, которые компилируются в С или в С++) объявляет size_t как глобальное им я типа. Для С++, подключение соответствующих хэдеров (в программы, которые компилируются только в С++) объявляет size_t как член пространства std.

По определению, size_t — это тип результата оператора sizeof. Таким образом, наиболее правильный путь объявить n — выполнить присваивание:

n = sizeof(thing);

Таким образом, будет достигнуты и портабельность кода программы, и ее эффективность. Соответственно, наиболее правильный способ объявить некую функцию foo следующий:

foo(sizeof(thing));

Здесь также будут достигнуты и переносимость и эффективность.

Функции, в которых присутствуют параметры типа size_t, часто имеют локальные переменные, которые являются индексами массивов. Использование в качестве типа size_t для индексов — это тоже хорошее решение.

Таким образом, использование size_t делает код более-менее само-документированным. Когда вы видите объект, объявленный как size_t, вы сразу понимаете, что он собой представляет — либо размер в байтах, либо индекс, но никак не код ошибки или какое-нибудь арифметическое целочисленное значение.

Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. For more information about Dan Saks, visit his website at www.dansaks.com. Dan also welcomes your feedback: e-mail him at dsaks@wittenberg.edu. For more information about Dan click here .

_________________________
Еще одно мое примечание.

Честно говоря, я не очень доволен этой статьей. Наверно мне следовало прочитать ее, и донести проблему уже своими словами и с учетом исправленных неточностей, а не делать перевод. Хотя, кое-какие интересные мысли в этой статье все же есть.

Интересно было почитать комментарии к статье. Что называется, на автора читатели «наехали», поколотили его. И правильно сделали!

Не смотря на небезгрешность статьи, я решил ее всё же перевести и опубликовать. Ведь о проблеме и применении size_t лучше знать, чем не знать. Хотя еще раз повторюсь, статья — не очень. Ниже среднего.

В качестве компенсации и дополнения я даю ссылку на другую статью на тему size_t:

http://www.viva64.com/ru/t/0044/

Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s