programing

형식 안전 열거 형을 만드는 방법은 무엇입니까?

kingscode 2021. 1. 15. 08:27
반응형

형식 안전 열거 형을 만드는 방법은 무엇입니까?


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열거 형 상수에 대해서도 완전한 유형 안전성을 달성 할 수 있는 이러한 함수 또는 매크로를 작성하는 방법이 있습니까?


몇 가지 트릭으로이를 달성 할 수 있습니다. 주어진

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);

정답은 꽤 좋지만 컴파일을 위해 많은 C99 및 C11 기능 세트가 필요하다는 단점이 있습니다. 게다가 할당이 매우 부자연 스럽습니다. 매직 color_assign()함수 또는 매크로 를 사용해야합니다 . 표준 =연산자 대신 데이터를 이동합니다 .

(분명히 질문은 작성 방법대해 명시 적으로 물 color_assign()었지만 질문을 더 광범위하게 살펴보면 실제로는 어떤 형태의 열거 형 상수로 형식 안전성을 얻기 위해 코드를 변경하는 방법에 대한 것이므로 필요하지 않은 것으로 간주합니다 color_assign(). 형식 안전성을 얻는 첫 번째 장소는 대답에 대한 공정한 게임입니다.)

포인터는 C가 형식이 안전한 것으로 취급하는 몇 가지 모양 중 하나이므로이 문제를 해결하기위한 자연스러운 후보가됩니다. 그래서 저는 이런 식으로 공격 할 것입니다.를 사용하는 대신 enum, 고유하고 예측 가능한 포인터 값을 가질 수 있도록 약간의 메모리를 희생 한 다음 #define"열거 형"을 구성하기 위해 정말 펑키 한 명령문을 사용합니다 (예, 매크로가 매크로 네임 스페이스를 오염 시킨다는 것을 알고 있지만 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];

But from the consumer's perspective, this is all hidden: color_t is very nearly an opaque object. You can't assign anything to it other than valid color_t values and 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 */
}

This works well in most cases, but it does have the problem of not working in switch statements; you can't switch on a pointer (which is a shame). But if you're willing to add one more macro to make switching possible, you can arrive at something that's "good enough":

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;
    }
}

Is this a good solution? I wouldn't call it great, since it both wastes some memory and pollutes the macro namespace, and it doesn't let you use enum to automatically assign your color values, but it is another way to solve the problem that results in somewhat more natural usages, and unlike the top answer, it works all the way back to C89.


Ultimately, what you want is a warning or error when you use an invalid enumeration value.

As you say, the C language cannot do this. However you can easily use a static analysis tool to catch this problem - Clang is the obvious free one, but there are plenty of others. Regardless of whether the language is type-safe, static analysis can detect and report the problem. Typically a static analysis tool puts up warnings, not errors, but you can easily have the static analysis tool report an error instead of a warning, and change your makefile or build project to handle this.


One could enforce type safety with a 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  };

Since color is just a wrapped integer, it can be passed by value or by pointer as one would do with an int. With this definition of color, color_assign(&val, 3); fails to compile with:

error: incompatible type for argument 2 of 'color_assign'

     color_assign(&val, 3);
                        ^

Full (working) example:

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"
}

Play with in online (demo).

ReferenceURL : https://stackoverflow.com/questions/43043246/how-to-create-type-safe-enums

반응형