web-dev-qa-db-ja.com

タイプセーフな列挙型を作成する方法は?

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を記述する方法はありますか?

54
Lundin

いくつかのトリックでこれを達成することが可能です。与えられた

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は、ダミーのユニオンの一時的なインスタンスを作成します。指定された初期化構文を使用して、値BLUEBLUEという名前のユニオンメンバーに割り当てます。そのようなメンバーが存在しない場合、コードはコンパイルされません。
  • 次に、対応する型のユニオンメンバが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;と入力することを妨げません。このような列挙型の割り当てを使用する可能性を完全にブロックする場合は、構造体に入れて、"opaque type"でプライベートカプセル化の標準手順を使用できます。

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

一番上の答えはかなり良いですが、コンパイルするために多くのC99とC11の機能セットが必要になるという欠点があり、その上、割り当てがかなり不自然になります。魔法のcolor_assign()標準の_=_演算子の代わりにデータを移動するための関数またはマクロ。

(確かに、質問は明示的にhowについてcolor_assign()を書くように尋ねましたが、質問をもっと広く見ると、それは本当に列挙された定数の何らかの形で型安全性を得るためにコードを変更する方法、そして私は最初にcolor_assign()を必要としないことを検討します。

ポインターは、Cがタイプセーフとして扱う数少ない図形の1つであるため、この問題を解決するための自然な候補になります。したがって、この方法で攻撃します:enumを使用するのではなく、一意の予測可能なポインター値を得るために少しのメモリを犠牲にしてから、本当におもしろいファンキーな_#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以外には何も割り当てられません:

ser.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することはできません(これは残念です)。ただし、切り替えを可能にするためにもう1つマクロを追加する場合は、「十分な」ものに到達できます。

color.h

_...

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

ser.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;
    }
}
_

これはgoodソリューションですか?私はそれを呼び出さないでしょうgreat、それはメモリを浪費し、マクロ名前空間を汚染し、enumは、色の値を自動的に割り当てますが、それはisであり、問​​題を解決するもう1つの方法です。 C89までさかのぼって機能します。

8
Sean Werkema

最終的に、無効な列挙値を使用した場合の警告またはエラーが必要です。

あなたが言うように、C言語はこれを行うことができません。ただし、静的解析ツールを使用してこの問題を簡単に検出できます。Clangは明らかに無料のものですが、他にもたくさんあります。言語がタイプセーフかどうかに関係なく、静的分析は問題を検出して報告できます。通常、静的分析ツールはエラーではなく警告を表示しますが、静的分析ツールに警告の代わりにエラーを報告させ、メイクファイルまたはビルドプロジェクトを変更してこれを処理させることができます。

7
Graham

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);は次でコンパイルできません。

エラー: 'color_assign'の引数2の型は互換性がありません

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

オンライン(デモ)で再生

7
YSC