Подтвердить что ты не робот

Когда следует использовать только "int" по сравнению с другими типами, специфичными для конкретных типов или размера?

У меня есть небольшая виртуальная машина для языка программирования, реализованная на C. Она поддерживает компиляцию как в 32-битной, так и в 64-разрядной архитектурах а также как C, так и С++.

Я пытаюсь сделать его компилируемым с помощью максимально возможного количества предупреждений. Когда я включаю CLANG_WARN_IMPLICIT_SIGN_CONVERSION, я получаю каскад новых предупреждений.

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

Конечно, это смешение их - использование в основном int для таких вещей, как локальные переменные и параметры, и использование более узких типов для полей в структурах - вызывает множество проблем с неявным преобразованием.

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

Но, если я пытаюсь использовать более конкретные типы повсюду, я нахожусь в лабиринте твистских бросков повсюду.

Что делают другие проекты C?

4b9b3361

Ответ 1

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

  • int может быть короче, чем вы ожидаете. Несмотря на то, что на большинстве настольных платформ int обычно составляет 32 бита, стандарт C гарантирует минимальную длину 16 бит. Может ли ваш код когда-либо понадобиться число больше 2 16 & minus; 1 = 32,767, даже для временных значений? Если это так, не используйте int. (Вместо этого вы можете использовать long, а long - не менее 32 бит.)

  • Даже a long может не всегда быть достаточно длинным. В частности, нет гарантии, что длина массива (или строки, которая является char массив) помещается в long. Используйте size_t (или ptrdiff_t, если вам нужна подписанная разница).

    В частности, a size_t определяется как достаточно большой для хранения любого допустимого индекса массива, тогда как int или даже long может не быть быть. Таким образом, например, при итерации по массиву ваш счетчик циклов (и его начальные/конечные значения) обычно должен быть size_t, по крайней мере, если вы точно не знаете, что массив достаточно короток для работы меньшего типа. (Но будьте осторожны при итерации назад: size_t не имеет знака, поэтому for(size_t i = n-1; i >= 0; i--) является бесконечным циклом! Использование i != SIZE_MAX или i != (size_t) -1 должно работать, хотя или использовать цикл do/while, но будьте осторожны случая n == 0!)

  • Подписан int. В частности, это означает, что int переполнение - это undefined. Если когда-либо существует риск, что ваши ценности могут законно переполняться, не используйте int; вместо этого используйте unsigned int (или unsigned long или uintNN_t).

  • Иногда вам нужна фиксированная длина бит. Если вы взаимодействуете с ABI или читаете/записываете формат файла, для которого требуются целые числа определенной длины, тогда длину, которую вы должны использовать. (Конечно, это такие ситуации, вам также может понадобиться беспокоиться о таких вещах, как endianness, и поэтому иногда приходится прибегать к тому, чтобы вручную упаковывать данные по байтам в любом случае.)

Все, что было сказано, есть также причины, чтобы избежать использования типов фиксированной длины все время: не только int32_t неудобно вводить все время, но вынуждение компилятора всегда использовать 32-битные целые числа не всегда особенно на платформах, где собственный размер int может быть, скажем, 64 бита. Вы можете использовать, скажем, C99 int_fast32_t, но еще более неудобно печатать.


Таким образом, вот мои личные предложения по максимальной безопасности и переносимости:

  • Определите свои собственные целые типы для случайного использования в общем файле заголовка, примерно так:

    #include <limits.h>
    typedef int i16;
    typedef unsigned int u16;
    #if UINT_MAX >= 4294967295U
      typedef int i32;
      typedef unsigned int u32;
    #else
      typedef long i32;
      typedef unsigned long i32;
    #endif
    

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

    Удобно, типы u32 и u16, определенные, как указано выше, гарантируются как минимум как unsigned int и поэтому могут быть использованы безопасно, не беспокоясь о том, что они продвигается до int и вызывает поведение переполнения undefined.

  • Используйте size_t для всех размеров массивов и индексирования, но будьте осторожны при выборе между ним и любыми другими целыми типами. Необязательно, если вам не нравится набирать так много подчеркиваний, typedef более удобный псевдоним для него тоже.

  • Для вычислений, предполагающих переполнение с определенным количеством битов, используйте либо uintNN_t, либо просто используйте u16/u32, как определено выше, и явное битовое маскирование с помощью &. Если вы решите использовать uintNN_t, убедитесь, что вы защитили себя от непредвиденного продвижения до int; один способ сделать это с помощью макроса:

    #define u(x) (0U + (x))
    

    который должен позволить вам безопасно писать, например:

    uint32_t a = foo(), b = bar();
    uint32_t c = u(a) * u(b);  /* this is always unsigned multiply */
    
  • Для внешних ABI, для которых требуется определенная целая длина, снова определите конкретный тип, например:

    typedef int32_t fooint32;  /* foo ABI needs 32-bit ints */
    

    Опять же, это имя типа самодокументируется как по размеру, так и по назначению.

    Если ABI действительно может потребовать, скажем, 16- или 64-битных ints, в зависимости от параметров платформы и/или времени компиляции, вы можете изменить определение типа для соответствия (и переименовать тип только fooint) — но тогда вам действительно нужно быть осторожным, когда вы делаете что-либо из этого или из этого типа, потому что оно может неожиданно переполняться.

  • Если ваш код имеет свои собственные структуры или форматы файлов, для которых требуются конкретные длины бит, подумайте над определением настраиваемых типов для них, точно так же, как если бы это был внешний ABI. Или вы можете просто использовать uintNN_t вместо этого, но вы потеряете немного самодокументации таким образом.

  • Для всех этих типов не забудьте также определить соответствующие константы _MIN и _MAX для простой проверки границ. Это может показаться большой работой, но на самом деле это всего лишь пара строк в одном файле заголовка.

Наконец, помните, что будьте осторожны с целочисленной математикой, особенно с переполнением. Например, имейте в виду, что разность двух n-разрядных целых чисел со знаком может не соответствовать n-битовому int. (Он будет вписываться в n-бит unsigned int, если вы знаете его неотрицательным, но помните, что вам нужно вводить входы в неподписанный тип, прежде чем принимать их различие, чтобы избежать поведения undefined!). Аналогично, чтобы найти среднее значение двух целых чисел (например, для двоичного поиска), не используйте avg = (lo + hi) / 2, а скорее, например. avg = lo + (hi + 0U - lo) / 2; первая сломается, если сумма переполняется.

Ответ 2

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

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

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

Во всех других случаях, например. где вы хотите намеренно оставаться рядом с техническими характеристиками машины, int может и иногда должен быть оставлен. Типичные случаи включают в себя сетевые протоколы, в которых данные передаются по проводам, а средства взаимодействия - мосты между C и другими языками, процедуры сборки ядра, доступ к структурам C. Но не забывайте, что иногда вы действительно хотели бы использовать int даже в этих случаях, поскольку для платформ должен использоваться собственный "родной" или предпочтительный размер слова, и вы можете полагаться на это самое свойство.

С такими типами платформ, как uint32_t, ядро ​​может захотеть использовать эти (хотя и не обязательно) в своих структурах данных, если к ним обращаются как от C, так и от ассемблера, поскольку последний имеет проблемы с доступом к int как тип.

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

Что касается size_t и других типов "наводящий на размышления" - если синтаксис следует за семантикой, присущей типу, скажем, используя size_t для ну, значения размера всех видов - я бы не оспаривал. Но я бы не стал применять его ни к чему, только потому, что он гарантированно будет самым большим типом (независимо от того, действительно ли это так). Это подводный камень, на который вы не хотите наступать позже. Кодекс должен быть понятным, насколько это возможно, я бы сказал - имея size_t, где никто, естественно, не ожидал, поднимет брови по уважительной причине. Используйте size_t для размеров. Используйте offset_t для смещения. Используйте [u]intN_t для октетов, слов и т.д. И так далее.

Это касается применения семантики, присущей определенному типу C, вашему исходному коду и о последствиях для текущей программы.

Кроме того, как показали другие, не уклоняйтесь от typedef, так как он дает вам возможность эффективно определять ваши собственные типы - объект абстракции, который я лично ценю. Хороший исходный код программы может даже не выставлять одиночный int, тем не менее полагаясь на int, псевдоним за множеством целевых типов. Я не собираюсь освещать typedef здесь, остальные, надеюсь, будут.

Ответ 3

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

Для примера проекта, использующего size_t, обратитесь к GNU dd.c, строка 155.

Ответ 4

Вот несколько вещей, которые я делаю. Не уверен, что они для всех, но они работают для меня.

  • Не используйте int или unsigned int напрямую. Кажется, что для работы всегда есть более подходящий тип.
  • Если переменная должна быть определенной ширины (например, для аппаратного регистра или для соответствия протоколу), используйте тип, специфичный для ширины (например, uint32_t).
  • Для итераторов массивов, где я хочу получить доступ к элементам массива 0 до n, это также должно быть неподписанным (нет причин для доступа к индексу меньше 0), и я использую один из быстрых типов (например, uint_fast16_t), выбрав тип, основанный на минимальном размере, требуемом для доступа ко всем элементам массива. Например, если у меня есть цикл for, который будет выполнять итерацию через 24 элемента max, я буду использовать uint_fast8_t, и пусть компилятор (или stdint.h, в зависимости от того, как педантичный мы хотим получить), решает, что является самым быстрым типом для этой операции.
  • Всегда используйте неподписанные переменные, если нет конкретной причины для их подписания.
  • Если ваши неподписанные переменные и подписанные переменные должны играть вместе, используйте явные приведения и осознавайте последствия. (К счастью, это будет сведено к минимуму, если вы избежите использования подписанных переменных, кроме случаев, когда это абсолютно необходимо.)

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

Ответ 5

Всегда.

Если у вас нет конкретных причин для использования более конкретного типа, в том числе на 16-битной платформе, и вам нужны целые числа больше 32767 или вам необходимо обеспечить правильный порядок и вывеску байтов для обмена данными по сети или в файл (и если вы не ограничены ресурсами, рассмотрите перенос данных в "обычный текст", что означает ASCII или UTF8, если вы предпочитаете).

Мой опыт показал, что "просто использовать" int "- это хорошая максима для жизни и позволяет быстро выворачивать рабочий, легко поддерживаемый, исправлять код каждый раз. Но ваша конкретная ситуация может различаться, поэтому возьмите этот совет с немного заслуженным вниманием.