Есть ли способ сделать перечисление неподписанным в стандарте C90 (соответствует MISRA-C 2004)

Я пытаюсь найти способ сделать перечисление "без знака".

enum{
     x1 = 0,
     x2,
     x3
};
uint8_t = x2; /* <--- PC-LINT MISRA-C 2004 will complain about mixing signed and unsigned here */

Конечно, я могу добавить Typecast, чтобы избавиться от ошибки, которая занимает много времени и подвержена ошибкам.

uint8_t = (uint8_t)x2; /* This works, but is a lot of extra work over the course of 1000s of lines of code*/

Итак, есть ли способ сделать конкретное перечисление неподписанным, что MISRA-C 2004 понравится?

5 ответов

Решение

Не существует стандартного способа C для управления типом, выбранным для enum, Иногда это можно сделать способами, специфичными для реализации, например, добавив значение в перечисление, которое вынуждает тип быть беззнаковым:

enum {
  x1,
  x2,
  x3,
  giant_one_for_forcing_unsigned = 0x80000000;
};

Но это даже не стандартный C, так как предоставленное значение не помещается в int). К сожалению, вам не повезло. Вот соответствующий бит из стандарта:

6.7.2.2 Спецификаторы перечисления, пункт 4

Каждый перечисляемый тип должен быть совместим с char, целочисленный тип со знаком или целочисленный тип без знака. Выбор типа определяется реализацией, но должен быть способен представлять значения всех членов перечисления. Перечисляемый тип является неполным, пока сразу после } который завершает список объявлений перечислителя и завершается после этого.

Вы могли бы быть лучше, используя #define скорее, чем enum сделать ваши константы:

#define x1 0U
#define x2 1U
#define x3 2U

uint8_t x = x2;

Здесь есть несколько проблем, из-за которых существует небольшая вероятность ошибок конверсии, которых MISRA пытается избежать:

  • Enum константы, то есть x1 и т. д. в вашем примере гарантированно будет иметь тип int (1). Но переменные enum и enum типа переменных не гарантированно относятся к одному и тому же типу (2), если вам не повезло, он определен как маленький целочисленный тип и, таким образом, подчиняется правилам целочисленного продвижения.

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

Ваша конкретная ошибка соответствия MISRA на самом деле является следствием последней проблемы, описанной выше, нарушения правила 10.3 (3).

Вы можете решить эту проблему, добавив явное приведение к "базовому типу" (предполагаемому типу), в данном случае приведение к uint8_t. Или вы можете решить эту проблему, вообще не используя перечисления, замените их на #defines. Это может звучать очень радикально, но имейте в виду, что C не имеет никакой безопасности типов, поэтому нет очевидного преимущества использования перечислений, кроме, возможно, читабельности.

Несколько часто заменяют перечисления следующим образом:

#define FALSE 0
#define TRUE  1
typedef uint8_t BOOL;

(Хотя цель в этом примере в основном состоит в том, чтобы сделать тип BOOL переносимым, с гарантией, что он будет 8 битами, а не 16 битами, как могло бы случиться, если бы это было перечисление.)


Рекомендации:

(1) С11 6.2.7.7/2:

"Выражение, которое определяет значение константы перечисления, должно быть выражением целочисленной константы, значение которого представлено в виде целого числа".

(2) С11 6.2.7.7/4:

"Каждый перечислимый тип должен быть совместим с char, целочисленным типом со знаком или целочисленным типом без знака. Выбор типа определяется реализацией, но должен быть способен представлять значения всех членов перечисления".

(3) MISRA-c: правило 10.3 2004 года:

"Значение комплексного выражения целочисленного типа может быть приведено только к типу, который является более узким и имеет ту же подпись, что и базовый тип выражения".

Мало того, что в C90 нет способа указать, что enum возьмите без знака типа, но в C90:

Идентификатор, объявленный как константа перечисления, имеет тип int

Это также относится к C99 (6.4.4.3). Если вы хотите беззнаковый тип, вы смотрите на расширение языка.

Тип перечисления может отличаться от int, но сами константы должны иметь int тип.

Вы можете заставить его быть без знака, добавив значение, достаточно большое, чтобы оно не могло поместиться в int (согласно спецификации). Это довольно просто для типов>= sizeof int, но unsigned char/short более сложна и требует специфичной для компилятора упаковки. Конечно, технически реализации все еще могут представлять UINT_MAX как длинную без знака... не то, что я когда-либо видел.

#include <stdio.h> //only included for printf example
#include <limits.h>
#include <stdint.h>

/** set up some helper macros **/
#ifdef _MSC_VER 
    #define PACK( ... ) __pragma( pack(push, 1) ) __VA_ARGS__ __pragma( pack(pop) )
#else /* for gcc, clang, icc and others */
    #define PACK( ... ) __VA_ARGS__ __attribute__((__packed__))
#endif
#define _PASTE(x,y) x ## y
#define PASTE(x,y) _PASTE(x,y)

/* __LINE__ added for semi-unique names */
#define U_ENUM(n, ... ) \
    enum n { __VA_ARGS__ , PASTE( U_DUMMY , __LINE__ ) = UINT_MAX }
#define UL_ENUM(n, ... ) \
    enum n { __VA_ARGS__ , PASTE( UL_DUMMY , __LINE__ ) = ULONG_MAX }
#define SZ_ENUM(n, ... ) /* useful for array indices */ \
    enum n { __VA_ARGS__ , PASTE( SZ_DUMMY , __LINE__ ) = SIZE_MAX }
#define ULL_ENUM(n, ... ) \
    enum n { __VA_ARGS__ , PASTE( ULL_DUMMY , __LINE__ ) = ULLONG_MAX }
#define UC_ENUM(n,...) \
    PACK(enum n { __VA_ARGS__ , PASTE( UC_DUMMY , __LINE__ ) = UCHAR_MAX })
#define US_ENUM(n,...) \
    PACK(enum n { __VA_ARGS__ , PASTE( US_DUMMY , __LINE__ ) = USHRT_MAX })

Вот проверка, чтобы видеть, что это работает как ожидалось:

typedef UC_ENUM(,a) A_t;
typedef US_ENUM(,b) B_t;
typedef U_ENUM(,c) C_t;
typedef UL_ENUM(,d) D_t;
typedef ULL_ENUM(,e) E_t;
typedef SZ_ENUM(,e) F_t;
int main(void) {
  printf("UC %d,\nUS %d,\nU %d,\nUL %d,\nULL %d,\nSZ %d,\n",sizeof(A_t),
    sizeof(B_t),sizeof(C_t),sizeof(D_t),sizeof(E_t),sizeof(F_t));
  return 0;
}

Чтобы быть более похожим на стандартный оператор перечисления, это немного отличается от более простой версии, которую я использую, которая принимает дополнительный именованный параметр для последнего перечисления вместо __LINE__ hack (это также полезно для функций, которые возвращают -1 при ошибке, потому что приведёт к U*_MAX). Вот как выглядит эта версия:

#define U_ENUM( n, err, ...)      enum n { __VA_ARGS__ , err = UINT_MAX  }
#define UL_ENUM(n, err, ...)      enum n { __VA_ARGS__ , err = ULONG_MAX }
#define ULL_ENUM(n,err, ...)      enum n { __VA_ARGS__ , err = ULLONG_MAX}
#define SZ_ENUM(n, err, ...)      enum n { __VA_ARGS__ , err = SIZE_MAX  }
#define UC_ENUM(n, err, ...) PACK(enum n { __VA_ARGS__ , err = UCHAR_MAX })
#define US_ENUM(n, err, ...) PACK(enum n { __VA_ARGS__ , err = USHRT_MAX })

Помимо упаковки перечислений в char или short для компактности, перечисления size_t являются наиболее интересными, потому что они могут использоваться в качестве индексов массива без дополнительной инструкции MOV.

typedef SZ_ENUM(message_t,MSG_LAST,MSG_HELLO,MSG_GOODBYE,MSG_BAD) message_t;
static const char *messages[]={"hello","goodbye","bad message"};
void printmsg(message_t msg){
  if (msg > MSG_BAD) msg = MSG_BAD;
  (void) puts(messages[msg]);
}

Обратите внимание, если вы используете C++11 против C, вы можете enum Foo : char { A, B, C}; или же enum class Bar : size_t { X, Y, Z};

В дополнение к ответу @ Карла, чтобы получить некоторые из преимуществ enum объявление и результат в некотором типе без знака, код может использовать ниже.

// Form values 0, 5, 6
enum { 
 x1, 
 x2 = 5, 
 x3
};

// Form values 0u, 5u, 6u
#define ux1 (1u * x1)
#define ux2 (1u * x2)
#define ux3 (1u * x3)

Это может не помочь с константами перечисления вне int спектр.

Конечно, код может выполнить преобразование вместо этого, как знает OP.

// uint8_t = x2;
uint8_t = x2 * 1u;
Другие вопросы по тегам