C 中的类型安全
Type-safety in 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 结构来包装 unsigned
s 并达到预期的结果,我只是想知道是否有更优雅的方法来做到这一点。
能再多一点吗?
别名在 C 语言中具有非常具体的狭义含义,这不是您所想的。你可能想说"typedefing"。
答案是否定的,你不能。无论如何都不是优雅的方式。您可以为每种数字类型使用一个结构,并使用一组单独的函数对每种类型进行算术运算。除非涉及到乘法,否则你就不走运了。为了将英尺乘以磅,您需要第三种类型。您还需要平方英尺、立方英尺、秒的负二次方和无数其他类型的类型。
如果这就是您所追求的,C 不是正确的语言。
问题是 C 不会将您的两个 typedef 视为不同的类型,因为它们都是 unsigned
.
类型
有多种技巧可以避免这种情况。一件事是将您的类型更改为枚举。好的编译器会在隐式转换时强制执行更强的类型警告 to/from 某个枚举类型到任何其他类型。
即使您没有好的编译器,使用枚举也可以做到这一点:
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;
}
一个更 conventional/traditional 的技巧是使用通用结构包装器,其中您使用 "ticket" 枚举来标记类型。示例:
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 版本。缺点是代码和内存开销,它只允许 运行 次检查。
您需要在构建过程中使用静态分析工具来实现此目的。
例如,如果您在代码上 运行 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
编辑:如果您的编译器不支持 _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: In function ‘main’:
Edit1.c:17:10: error: incompatible type for argument 1 of ‘calc’
calc(amount); // raise warning
^
Edit1.c:10:6: note: expected ‘cent_t {aka struct }’ but argument is of type ‘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'
有没有办法让 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 结构来包装 unsigned
s 并达到预期的结果,我只是想知道是否有更优雅的方法来做到这一点。
能再多一点吗?
别名在 C 语言中具有非常具体的狭义含义,这不是您所想的。你可能想说"typedefing"。
答案是否定的,你不能。无论如何都不是优雅的方式。您可以为每种数字类型使用一个结构,并使用一组单独的函数对每种类型进行算术运算。除非涉及到乘法,否则你就不走运了。为了将英尺乘以磅,您需要第三种类型。您还需要平方英尺、立方英尺、秒的负二次方和无数其他类型的类型。
如果这就是您所追求的,C 不是正确的语言。
问题是 C 不会将您的两个 typedef 视为不同的类型,因为它们都是 unsigned
.
有多种技巧可以避免这种情况。一件事是将您的类型更改为枚举。好的编译器会在隐式转换时强制执行更强的类型警告 to/from 某个枚举类型到任何其他类型。
即使您没有好的编译器,使用枚举也可以做到这一点:
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;
}
一个更 conventional/traditional 的技巧是使用通用结构包装器,其中您使用 "ticket" 枚举来标记类型。示例:
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 版本。缺点是代码和内存开销,它只允许 运行 次检查。
您需要在构建过程中使用静态分析工具来实现此目的。
例如,如果您在代码上 运行 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
编辑:如果您的编译器不支持 _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: In function ‘main’: Edit1.c:17:10: error: incompatible type for argument 1 of ‘calc’ calc(amount); // raise warning ^ Edit1.c:10:6: note: expected ‘cent_t {aka struct }’ but argument is of type ‘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'