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

Как создать безопасные перечисления типов?

Для обеспечения безопасности типов с перечислениями в C проблематично, поскольку они по существу являются целыми числами. И константы перечисления на самом деле определены стандартом типа int.

Чтобы достичь некоторой степени безопасности, я делаю трюки с указателями, как это:

typedef enum
{
  BLUE,
  RED
} color_t;

void color_assign (color_t* var, color_t val) 
{ 
  *var = val; 
}

Поскольку указатели имеют более строгие правила типа, чем значения, поэтому это предотвращает такой код:

int x; 
color_assign(&x, BLUE); // compiler error

Но это не предотвращает такой код:

color_t color;
color_assign(&color, 123); // garbage value

Это связано с тем, что константа перечисления по существу является просто int и может быть неявно назначена переменной перечисления.

Есть ли способ написать такую ​​функцию или макрос color_assign, который может обеспечить полную безопасность типа даже для констант перечисления?

4b9b3361

Ответ 1

Это можно сделать с помощью нескольких трюков. Учитывая,

typedef enum
{
  BLUE,
  RED
} color_t;

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

typedef union
{
  color_t BLUE;
  color_t RED;
} typesafe_color_t;

Это возможно, потому что константы перечисления и имена элементов/переменных находятся в разных пространствах имен.

Затем создайте несколько макросов, соответствующих функциям:

#define c_assign(var, val) (var) = (typesafe_color_t){ .val = val }.val
#define color_assign(var, val) _Generic((var), color_t: c_assign(var, val))

Эти макросы затем вызываются так:

color_t color;
color_assign(color, BLUE); 

Объяснение:

  • Ключевое слово C11 _Generic гарантирует, что переменная перечисления имеет правильный тип. Однако это нельзя использовать для константы перечисления BLUE, поскольку она имеет тип int.
  • Поэтому вспомогательный макрос c_assign создает временный экземпляр фиктивного объединения, где назначенный синтаксис инициализатора используется для присвоения значения BLUE члену объединения с именем BLUE. Если такой элемент не существует, код не будет компилироваться.
  • Член объединения соответствующего типа затем копируется в переменную enum.

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

#define color_assign(var, val) _Generic((var), \
color_t: (var) = (typesafe_color_t){ .val = val }.val )

Примеры:

color_t color; 
color_assign(color, BLUE);// ok
color_assign(color, RED); // ok

color_assign(color, 0);   // compiler error 

int x;
color_assign(x, BLUE);    // compiler error

typedef enum { foo } bar;
color_assign(color, foo); // compiler error
color_assign(bar, BLUE);  // compiler error

ИЗМЕНИТЬ

Очевидно, что это не помешает вызывающему абоненту просто ввести color = garbage;. Если вы хотите полностью заблокировать возможность использования такого назначения перечисления, вы можете поместить его в структуру и использовать стандартную процедуру частного инкапсуляции с "непрозрачным типом":

color.h

#include <stdlib.h>

typedef enum
{
  BLUE,
  RED
} color_t;

typedef union
{
  color_t BLUE;
  color_t RED;
} typesafe_color_t;

typedef struct col_t col_t; // opaque type

col_t* col_alloc (void);
void   col_free (col_t* col);

void col_assign (col_t* col, color_t color);

#define color_assign(var, val)   \
  _Generic( (var),               \
    col_t*: col_assign((var), (typesafe_color_t){ .val = val }.val) \
  )

color.c

#include "color.h"

struct col_t
{
  color_t color;
};

col_t* col_alloc (void) 
{ 
  return malloc(sizeof(col_t)); // (needs proper error handling)
}

void col_free (col_t* col)
{
  free(col);
}

void col_assign (col_t* col, color_t color)
{
  col->color = color;
}

main.c

col_t* color;
color = col_alloc();

color_assign(color, BLUE); 

col_free(color);

Ответ 2

Верхний ответ довольно хорош, но у него есть недостатки, для которых требуется множество функций C99 и C11 для компиляции, и, кроме того, он делает назначение довольно неестественным: вы должны использовать магию color_assign() или макрос, чтобы перемещать данные вместо стандартного оператора =.

(По общему признанию, вопрос явно задавался вопросом о том, как писать color_assign(), но если вы посмотрите на вопрос более широко, это действительно о том, как изменить свой код, чтобы получить безопасность типа с некоторой формой перечислимых констант, а я 'd рассмотреть не нужно color_assign(), в первую очередь, чтобы получить безопасность типа, чтобы быть честной игрой для ответа.)

Указатели относятся к числу немногих форм, которые C рассматривает как безопасные для типа, поэтому они делают естественным кандидатом для решения этой проблемы. Поэтому я бы атаковал так: вместо того, чтобы использовать enum, я пожертвовал бы небольшой памятью, чтобы иметь уникальные, предсказуемые значения указателя, а затем использовать некоторые действительно hokey funky #define для построения моего "enum" (да, я знаю, что макросы загрязняют пространство имен макросов, но enum загрязняет глобальное пространство имен компилятора, поэтому я считаю его близким к четной сделке):

color.h

typedef struct color_struct_t *color_t;

struct color_struct_t { char dummy; };

extern struct color_struct_t color_dummy_array[];

#define UNIQUE_COLOR(value) \
    (&color_dummy_array[value])

#define RED    UNIQUE_COLOR(0)
#define GREEN  UNIQUE_COLOR(1)
#define BLUE   UNIQUE_COLOR(2)

enum { MAX_COLOR_VALUE = 2 };

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

color.c

#include "color.h"

/* This never actually gets used, but we need to declare enough space in the
 * BSS so that the pointer values can be unique and not accidentally reused
 * by anything else. */
struct color_struct_t color_dummy_array[MAX_COLOR_VALUE + 1];

Но с точки зрения потребителя все это скрыто: color_t - почти непрозрачный объект. Вы не можете присваивать ему ничего, кроме действительных значений color_t и NULL:

user.c

#include <stddef.h>
#include "color.h"

void foo(void)
{
    color_t color = RED;    /* OK */
    color_t color = GREEN;  /* OK */
    color_t color = NULL;   /* OK */
    color_t color = 27;     /* Error/warning */
}

Это хорошо работает в большинстве случаев, но у него есть проблема не работать в операторах switch; вы не можете switch на указателе (что является позором). Но если вы захотите добавить еще один макрос для возможности переключения, вы можете прийти к чему-то, что "достаточно хорошо":

color.h

...

#define COLOR_NUMBER(c) \
    ((c) - color_dummy_array)

user.c

...

void bar(color_t c)
{
    switch (COLOR_NUMBER(c)) {
        case COLOR_NUMBER(RED):
            break;
        case COLOR_NUMBER(GREEN):
            break;
        case COLOR_NUMBER(BLUE):
            break;
    }
}

Это хорошее решение? Я бы не назвал это замечательным, так как он отнимает некоторую память и загрязняет пространство имен макросов, и он не позволяет использовать enum для автоматического назначения ваших значений цвета, но это еще один способ решить проблему, которая приводит к несколько более естественных применений, и в отличие от верхнего ответа, он полностью возвращается к C89.

Ответ 3

Можно обеспечить безопасность типов с помощью struct:

struct color { enum { THE_COLOR_BLUE, THE_COLOR_RED } value; };
const struct color BLUE = { THE_COLOR_BLUE };
const struct color RED  = { THE_COLOR_RED  };

Так как color является просто завернутым целым числом, его можно передать по значению или указателем, как это было бы с int. С этим определением color, color_assign(&val, 3); не скомпилируется с помощью:

error: несовместимый тип для аргумента 2 из 'color_assign'

     color_assign(&val, 3);
                        ^

Полный (рабочий) пример:

struct color { enum { THE_COLOR_BLUE, THE_COLOR_RED } value; };
const struct color BLUE = { THE_COLOR_BLUE };
const struct color RED  = { THE_COLOR_RED  };

void color_assign (struct color* var, struct color val) 
{ 
  var->value = val.value; 
}

const char* color_name(struct color val)
{
  switch (val.value)
  {
    case THE_COLOR_BLUE: return "BLUE";
    case THE_COLOR_RED:  return "RED";
    default:             return "?";
  }
}

int main(void)
{
  struct color val;
  color_assign(&val, BLUE);
  printf("color name: %s\n", color_name(val)); // prints "BLUE"
}

Играть в онлайн (демо).

Ответ 4

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

Как вы говорите, язык C не может этого сделать. Однако вы можете легко использовать инструмент статического анализа, чтобы поймать эту проблему - Clang является очевидным бесплатным, но есть много других. Независимо от того, является ли язык безопасным по типу, статический анализ может обнаруживать и сообщать о проблеме. Как правило, инструмент статического анализа содержит предупреждения, а не ошибки, но вы можете легко заставить инструмент статического анализа сообщать об ошибке вместо предупреждения и изменять свой проект makefile или build для его обработки.

Ответ 5

Вот мое решение. Я просуммировал все, поэтому мои перечисления будут называться примерно так:

ns_a_e //ns = namespace; a=enum name; e = it an enum

и его член будет обладать тем, что:

enum ns_a_e {
    ns_a_e__a,
    ns_a_e__b,
    ns_a_e__c,
};

Итак, я решил, что могу изменить это выше:

typedef struct {
    enum {
        ns_a_e__a,
        ns_a_e__b,
        ns_a_e__c,
    } x; /*note: C doesn't scope the enumerators*/
} ns_a_e;

и я мог бы создать экземпляры с чем-то вроде:

#define NS_e(En,Val) (En){En##__##Val}

или (псевдо)) enum-specific

#define NS_a_e(Val) NS_e(ns_a_e,Val)

Эта простая модификация, в которой я пишу NS_a_e(a) вместо моего предыдущего ns_a_e__a делает эти (псевдо-) перечисления совершенно безопасными:

ns_a_e x;
x = NS_a_e(a);
x = NS_a_e(b);
//x = NS_a_e(foo); //ERROR
//x = 1; //ERROR

до тех пор, пока другие места моего кода соблюдают область видимости (= символы формы ns_a_e__* принадлежат ns_a_e)

Структурная упаковка не влияет на сгенерированную сборку на x86-64.

Полный пример (скомпилированный с -std=c90):

#define NS_e(En,X) (En){En##__##X}

/////////////////////////
typedef struct{
    enum {
        ns_a_e__a,
        ns_a_e__b,
        ns_a_e__c,
    } x;
} ns_a_e;
#define NS_a_e(X) NS_e(ns_a_e, X)


int main()
{
    ns_a_e x;
    x = NS_a_e(a);
    x = NS_a_e(b);
    //x = NS_a_e(foo); //ERROR
    //x = 1; //ERROR
}