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);
説明:
_Generic
キーワードは、列挙変数が正しい型であることを保証します。ただし、これはBLUE
型であるため、列挙定数int
では使用できません。c_assign
は、ダミーのユニオンの一時的なインスタンスを作成します。指定された初期化構文を使用して、値BLUE
をBLUE
という名前のユニオンメンバーに割り当てます。そのようなメンバーが存在しない場合、コードはコンパイルされません。実際にはヘルパーマクロは必要ありません。読みやすくするために式を分割しています。書くのと同じようにうまくいきます
#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);
一番上の答えはかなり良いですが、コンパイルするために多くの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までさかのぼって機能します。
最終的に、無効な列挙値を使用した場合の警告またはエラーが必要です。
あなたが言うように、C言語はこれを行うことができません。ただし、静的解析ツールを使用してこの問題を簡単に検出できます。Clangは明らかに無料のものですが、他にもたくさんあります。言語がタイプセーフかどうかに関係なく、静的分析は問題を検出して報告できます。通常、静的分析ツールはエラーではなく警告を表示しますが、静的分析ツールに警告の代わりにエラーを報告させ、メイクファイルまたはビルドプロジェクトを変更してこれを処理させることができます。
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"
}
_