web-dev-qa-db-ja.com

Cでの型安全性

Cに型をもう少し認識させ、型の安全性を保証する方法はありますか?
このことを考慮:

typedef unsigned cent_t;
typedef unsigned dollar_t;

#define DOLLAR_2_CENT(dollar)       ((cent_t)(100*(dollar)))

void calc(cent_t amount) {
    // expecting 'amount' to semantically represents cents...
}

int main(int argc, char* argv[]) {
    dollar_t amount = 50;
    calc(DOLLAR_2_CENT(amount));  // ok
    calc(amount);                 // raise warning
    return 0;
}

上記のコードを少なくともgccで警告を発生させる方法はありますか?
C構造体を使用してunsignedsをラップし、目的の結果を得ることができることを知っています。もっとエレガントな方法があるかどうか疑問に思っていました。
それだけでは足りませんか?

36
so.very.tired

これを実現するには、ビルドプロセスで静的分析ツールを使用する必要があります。

たとえば、コードでPCLintを実行すると、次の出力が得られます。

  [Warning 632] Assignment to strong type 'cent_t' in context: arg. no. 1
  [Warning 633] Assignment from a strong type 'dollar_t' in context: arg. no. 1

http://www.gimpel.com/html/strong.htm

8
Andy Sinclair

問題は、2つのtypedefがどちらもunsigned型であるため、Cが2つのtypedefを区別できる型として扱わないことです。

これを回避するためのさまざまなトリックがあります。 1つは、型を列挙型に変更することです。優れたコンパイラーは、特定の列挙型と他の型の間の暗黙の変換に対して、より強い型付け警告を強制します。

あなたが良いコンパイラを持っていなくても、列挙型でこれを行うことができます:

typedef enum { FOO_CENT  } cent_t;
typedef enum { FOO_DOLLAR} dollar_t;

#define DOLLAR_2_CENT(dollar)       ((cent_t)(100*(dollar)))

void calc(cent_t amount) {
    // expecting 'amount' to semantically represents cents...
}

#define type_safe_calc(amount) _Generic(amount, cent_t: calc(amount))

int main(int argc, char* argv[]) {
    dollar_t amount = 50;
    type_safe_calc(DOLLAR_2_CENT(amount));  // ok
    type_safe_calc(amount);         // raise warning

    return 0;
}

より一般的/伝統的なトリックは、型をマークするために「チケット」列挙型を使用する、汎用の構造体ラッパーを使用することです。例:

typedef struct
{
  type_t type;
  void*  data;
} wrapper_t;

...

cent_t my_2_cents;
wrapper_t wrapper = {CENT_T, &my_2_cents};

...

switch(wrapper.type)
{
  case CENT_T: calc(wrapper.data)
  ...
}

利点は、どのCバージョンでも動作することです。欠点は、コードとメモリのオーバーヘッドであり、実行時のチェックしかできないことです。

30
Lundin

エイリアシングは、Cで非常に特定の狭い意味を持ち、それはあなたが考えているものではありません。 「typedefing」と言いたいかもしれません。

そして答えはノーです、あなたはできません。とにかくエレガントな方法ではありません。数値型ごとに構造体を使用し、個別の関数セットを使用してそれぞれの数値を計算できます。掛け算に関しては、運が悪い。フィートをポンドで乗算するには、3番目のタイプが必要です。また、フィートの2乗、フィートの3乗、秒のマイナス2の累乗、および無限の他のタイプのタイプも必要です。

これがあなたの望んでいることなら、Cは適切な言語ではありません。

10

編集:コンパイラが_Genericセレクターをサポートしていない場合に備えて、C89でも機能する代替案(多くのコンパイラーはサポートしておらず、多くの場合、マシンにインストールされているもので立ち往生しています)。

マクロを使用すると、structラッパーの使用を簡略化できます。

#define NEWTYPE(nty,oty) typedef struct { oty v; } nty
#define FROM_NT(ntv)       ((ntv).v)
#define TO_NT(nty,val)     ((nty){(val)})  /* or better ((nty){ .v=(val)}) if C99 */


NEWTYPE(cent_t, unsigned);
NEWTYPE(dollar_t, unsigned);

#define DOLLAR_2_CENT(dollar)       (TO_NT(cent_t, 100*FROM_NT(dollar)))

void calc(cent_t amount) {
     // expecting 'amount' to semantically represents cents...
}  

int main(int argc, char* argv[]) {
    dollar_t amount = TO_NT(dollar_t, 50);  // or alternatively {50};
    calc(DOLLAR_2_CENT(amount));  // ok
    calc(amount);                 // raise warning
    return 0;
}

警告よりも強くなります。これはgcc 5.1でのコンパイル結果です

 $ gcc -O3 -Wall Edit1.c 
 Edit1.c:関数「main」内:
 Edit1.c:17:10: エラー: ‘calc’ 
 calc(amount);の引数1に互換性のないタイプ。 //警告を出します
 ^ 
 Edit1.c:10:6: 注意: ‘cent_t {aka struct}’が必要ですが、引数のタイプは ‘dollar_t {aka struct}’です
 void calc(cent_t amount); // {

そしてここにgcc 3.4での結果

 $ gcc -O3 -Wall Edit1.c 
 Edit1.c:In function 'main':
 Edit1.c:17:error:incompatible type for argument 1 of 'calc '
5