4.6 – Целочисленные типы фиксированной ширины и size_t

Добавлено 1 мая 2021 в 10:12

В предыдущих уроках, посвященных целочисленным типам, мы говорили о том, что C++ гарантирует только то, что целочисленные переменные будут иметь минимальный размер, но он может быть и больше, в зависимости от целевой системы.

Почему размер целочисленных переменных не фиксирован?

Короткий ответ: это восходит к C, когда компьютеры были медленными, а производительность вызывала наибольшую озабоченность. C решил намеренно оставить размер целого числа открытым, чтобы разработчики компиляторов могли выбрать размер для int, который лучше всего работает на архитектуре целевого компьютера.

Разве это не отстой?

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

Целочисленные типы фиксированной ширины

Чтобы облегчить кроссплатформенную переносимость, C99 определил набор целочисленных типов фиксированной ширины (в заголовочном файле stdint.h), которые гарантированно будут иметь одинаковый размер в любой архитектуре.

Они определены следующим образом:

НазваниеТипДиапазон значенийПримечание
std::int8_t1 байт со знакомот -128 до 127Во многих системах обрабатывается как signed char. Смотрите примечание ниже.
std::uint8_t 1 байт без знакаот 0 до 255Во многих системах обрабатывается как unsigned char. Смотрите примечание ниже.
std::int16_t2 байта со знакомот -32 768 до 32 767 
std::uint16_t2 байта без знакаот 0 до 65 535 
std::int32_t4 байта со знаком- от -2 147 483 648 до 2 147 483 647 
std::uint32_t4 байта без знакаот 0 до 4 294 967 295 
std::int64_t8 байт со знакомот -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 
std::uint64_t8 байт без знакаот 0 до 18 446 744 073 709 551 615 

C++ официально принял эти целочисленные типы фиксированной ширины как часть стандарта C++11. К ним можно получить доступ, включив заголовочный файл cstdint, где они определены внутри пространства имен std. Например:

#include <cstdint>
#include <iostream>
 
int main()
{
    std::int16_t i{5};
    std::cout << i;
    return 0;
}

Целочисленные типы фиксированной ширины имеют два недостатка: во-первых, они являются необязательными и существуют только в том случае, если есть базовые типы, соответствующие их ширине и следующие определенному двоичному представлению. Использование целочисленного типа фиксированной ширины делает ваш код менее портируемым, он может не компилироваться в других системах.

Во-вторых, если вы используете целочисленный тип фиксированной ширины, на некоторых архитектурах он может быть медленнее, чем более широкий тип. Если вам нужен целочисленный тип для хранения значений от -10 до 20, у вас может возникнуть соблазн использовать std::int8_t. Но ваш процессор мог бы лучше обрабатывать 32-битные целые числа, поэтому вы просто потеряли скорость, сделав ограничение, в котором не было необходимости.

Предупреждение


Приведенных выше целочисленных типов фиксированной ширины следует избегать, поскольку они могут не быть определены на всех целевых архитектурах.

Быстрые и наименьшие по размеру целочисленные типы

Чтобы помочь устранить указанные выше недостатки, C++ также определяет два альтернативных набора целочисленных типов.

Быстрый тип (std::int_fast#_t) обеспечивает самый быстрый целочисленный тип со знаком с шириной не менее # бит (где # = 8, 16, 32 или 64). Например, std::int_fast32_t предоставит вам самый быстрый целочисленный тип со знаком, имеющий как минимум 32 бита.

Наименьший по размеру тип (std::int_least#_t) предоставляет наименьший по размеру целочисленный тип со знаком с шириной не менее # бит (где # = 8, 16, 32 или 64). Например, std::int_least32_t предоставит вам наименьший целочисленный тип со знаком, имеющий как минимум 32 бита.

Вот пример программы, скомпилированной автором в Visual Studio (32-разрядное консольное приложение):

#include <cstdint>
#include <iostream>
 
int main()
{
	std::cout << "fast 8: " << sizeof(std::int_fast8_t) * 8 << " bits\n";
	std::cout << "fast 16: " << sizeof(std::int_fast16_t) * 8 << " bits\n";
	std::cout << "fast 32: " << sizeof(std::int_fast32_t) * 8 << " bits\n";
 
	std::cout << "least 8: " << sizeof(std::int_least8_t) * 8 << " bits\n";
	std::cout << "least 16: " << sizeof(std::int_least16_t) * 8 << " bits\n";
	std::cout << "least 32: " << sizeof(std::int_least32_t) * 8 << " bits\n";
 
	return 0;
}

В результате эта программа дает следующий вывод:

fast 8: 8 bits
fast 16: 32 bits
fast 32: 32 bits
least 8: 8 bits
least 16: 16 bits
least 32: 32 bits

Вы можете видеть, что std::int_fast16_t был 32-битным, тогда как std::int_least16_t был 16-битным.

Существует также набор быстрых и минимальных по размеру типов без знака (std::uint_fast#_t и std::uint_least#_t).

Эти быстрые и минимальные по размеру типы гарантированно определены и безопасны в использовании.

Лучшая практика


Выбирайте целочисленные типы std::int_fast#_t и std::int_least#_t, когда вам нужно, чтобы целое число гарантированно было не менее определенного минимального размера.

Предупреждение: std::int8_t и std::uint8_t могут вести себя как символы вместо целых чисел

Примечание: о символах мы подробнее поговорим в уроке «4.11 – Символы».

Из-за упущения в спецификации C++ большинство компиляторов определяют и обрабатывают std::int8_t и std::uint8_t (и соответствующие быстрые и наименьшие по размеру фиксированные типы) идентично типам signed char и unsigned char соответственно. Следовательно, std::cin и std::cout могут работать иначе, чем вы ожидаете. Ниже приведен пример программы, показывающей это:

#include <cstdint>
#include <iostream>
 
int main()
{
    std::int8_t myint{65};
    std::cout << myint;
 
    return 0;
}

В большинстве систем эта программа будет печатать 'A' (обрабатывая myint как символ). Однако в некоторых системах может быть напечатано 65, как и ожидалось.

Для простоты лучше вообще избегать std::int8_t и std::uint8_t (и связанных с ними быстрых и наименьших типов) (вместо этого используйте std::int16_t или std::uint16_t). Однако если вы всё же используете std::int8_t или std::uint8_t, вам следует быть осторожными со всем, что могло бы интерпретировать std::int8_t или std::uint8_t как символ вместо целого числа (включая std::cout и std::cin).

Надеюсь, это будет разъяснено в будущем черновике стандарта C++.

Предупреждение


Избегайте 8-битных целочисленных типов фиксированной ширины. Если вы их используете, обратите внимание, что они часто обрабатываются как символы.

Лучшие практики работы с целочисленными типами

Теперь, когда в C++ добавлены целочисленные типы фиксированной ширины, передовой практикой для целочисленных значений в C++ является следующее:

  • Следует предпочесть использование int, когда размер целого числа не имеет значения (например, число всегда будет соответствовать диапазону значений 2-байтового целочисленного типа со знаком). Например, если вы просите пользователя ввести свой возраст или считаете от 1 до 10, не имеет значения, равен ли int 16 или 32 битам (числа подходят в любом случае). Это покроет подавляющее большинство случаев, с которыми вы, вероятно, столкнетесь.
  • Если вам нужна переменная гарантированно определенного размера, и вы хотите повысить производительность, используйте std::int_fast#_t.
  • Если вам нужна переменная, гарантированно имеющая определенный размер, и вы хотите отдать предпочтение экономии памяти над производительностью, используйте std::int_least#_t. Он чаще всего используется при распределении большого количества переменных.

По возможности избегайте следующего:

  • Беззнаковые типы, если у вас нет веской причины.
  • 8-битные целочисленные типы фиксированной ширины.
  • Любые специфичные для компилятора целочисленные типы фиксированной ширины – например, Visual Studio определяет __int8, __int16 и т.д.

Что такое std::size_t?

Рассмотрим следующий код:

#include <iostream>
 
int main()
{
    std::cout << sizeof(int) << '\n';
 
    return 0;
}

На машине автора эта программа печатает:

4

Довольно просто, правда? Мы можем сделать вывод, что оператор sizeof возвращает целочисленное значение, но какой целочисленный тип у этого значения? int? short? Ответ заключается в том, что sizeof (и многие функции, возвращающие значение размера или длины) возвращают значение типа std::size_t. std::size_t определяется как целочисленный тип без знака и обычно используется для представления размера или длины объектов.

Забавно, но мы можем использовать оператор sizeof (который возвращает значение типа std::size_t), чтобы запросить размер самого std::size_t:

#include <cstddef> // std::size_t
#include <iostream>
 
int main()
{
	std::cout << sizeof(std::size_t) << '\n';
 
	return 0;
}

Скомпилированная как 32-битное (4 байтовое) консольное приложение в системе автора данная программа выводит:

4

Подобно целочисленному типу, размер которого зависит от системы, размер std::size_t также может быть разным. std::size_t гарантированно является беззнаковым и имеет не менее 16 бит, но в большинстве систем будет эквивалентен ширине адреса в приложении. То есть для 32-разрядных приложений std::size_t обычно будет 32-разрядным целочисленным типом без знака, а для 64-разрядного приложения size_t обычно будет 64-разрядным целочисленным типом без знака. size_t определяется достаточно большим, чтобы вместить размер самого большого объекта, созданного в вашей системе (в байтах). Например, если std::size_t имеет ширину 4 байта, самый большой объект, создаваемый в вашей системе, не может быть больше 4 294 967 295 байтов, потому что это наибольшее число, которое может хранить 4-байтовое целое число без знака. Это только верхний предел размера объекта, реальный предел размера может быть ниже в зависимости от используемого компилятора.

По определению, любой объект, размер которого превышает максимальное значение, которое может содержать size_t, считается неправильно сформированным (и вызовет ошибку компиляции), поскольку оператор sizeof не сможет вернуть размер без выполнения циклического переноса.

Теги

C++ / Cppsize_tsizeofДля начинающихОбучениеПрограммированиеЦелочисленный тип данных

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.