Как создать безопасные перечисления типов?
Для обеспечения безопасности типов с перечислениями в 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
, который может обеспечить полную безопасность типа даже для констант перечисления?
Ответы
Ответ 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
}