我有两个结构:A包含a,b,c,d作为成员,B包含b,c,d作为成员。我有多个可以传递A或B的 API。
typedef struct {
int a;
int b;
int c;
int d;
} A;
typedef struct {
int b;
int c;
int d;
} B;
Set_b(struct A, int);
Set_c(struct A, int);
Set_d(struct A, int);
Set_b'(struct B, int);
Set_c'(struct B, int);
Set_d'(struct B, int);
在C 中实现相同通用 API 的最简单方法是什么?
Set_b(X, int);
Set_c(X, int);
Set_d(X, int);
我不允许使用union
,因为代码必须符合 MISRA C。
C11 支持类型泛型表达式,它应该可以满足您的需求:
#define Set_b(X, y) _Generic((X), A: Set_b_A,
B: Set_b_B
)((X), (y))
然后,只需为适当的类型实现Set_b_A
和Set_b_B
。
您可以创建接口。例如
struct A {}; // As you have defined
void set_b_for_A(struct A, int) {} // function that works with A
// interface
struct helper {
void *ptr; // pointer to your structs variants (A, ...)
void (*set_b)(struct helper *, int); // helper to dispatch to correct worker
};
void set_b_helper_for_A(struct helper *pointer, int i) { // helper for worker A
struct A *s = (struct A *) pointer->ptr;
set_b_for_A(*s, i);
}
struct helper helper_A {/* A struct */, set_b_helper_for_A};
现在您的 API
void set_b(struct helper *ptr, int i) {
ptr->set_b(ptr, i);
}
例如,您调用:
set_b(&helper_A, 0);
对其他结构执行相同的操作
我会这样做。它是可调试的,不会生成太多代码(memcpy 将在任何优化级别上从实际代码中优化出来)https://godbolt.org/z/lMShik
顺便说一句,在这种情况下,IMO 不安全的版本与 IMO 相同安全,它确实违反了严格的别名规则。
typedef enum
{
TYPE_A,
TYPE_B,
TYPE_C
}Types;
struct a
{
int a;
int b;
int c;
int d;
};
struct b
{
int a;
int b;
int c;
};
struct c
{
int a;
int b;
};
void inline __attribute__((always_inline)) Set_a(void *ptr, Types type, int value)
{
struct a a;
struct b b;
struct c c;
switch(type)
{
case TYPE_A:
memcpy(&a, ptr, sizeof(a));
a.a = value;
memcpy(ptr, &a, sizeof(a));
break;
case TYPE_B:
memcpy(&b, ptr, sizeof(b));
b.a = value;
memcpy(ptr, &b, sizeof(b));
break;
case TYPE_C:
memcpy(&c, ptr, sizeof(c));
c.a = value;
memcpy(ptr, &c, sizeof(c));
break;
}
}
void inline __attribute__((always_inline)) Set_a_unsafe(void *ptr, Types type, int value)
{
struct a a;
struct b b;
struct c c;
switch(type)
{
case TYPE_A:
((struct a *)ptr) -> a = value;
break;
case TYPE_B:
((struct b *)ptr) -> a = value;
break;
case TYPE_C:
((struct c *)ptr) -> a = value;
break;
}
}
struct a x,y;
int main()
{
Set_a(&x, TYPE_A, 45);
Set_a_unsafe(&y, TYPE_B, 100);
printf("%dn", x.a);
printf("%dn", y.a);
}
可以使用几种不同的替代方法,但是这取决于您拥有什么,可以确定什么,而不是您不拥有什么,不能修改或更改。实际实现可能取决于 API 与structB
一起使用structA
的频率。您确实没有为明确建议的方法提供足够的信息。
看来您的发布可以重述如下。
有两个结构,structA
和structB
具有共同的成员。这些公共成员存储相同类型的数据,并且在检查时,整个structB
包含在structA
中,如下所示:
typedef struct {
int a;
int b; // beginning of portion that is same as structB below.
int c;
int d; // end of portion that is same as structB below.
int e;
} structA;
typedef struct {
int b; // same type of data as in b member of structA above
int c; // same type of data as in c member of structA above
int d; // same type of data as in d member of structA above
} structB;
举一个具体的例子,structA
描述了某个 3D 空间中的一个对象,其位置是一个 x,y,z 元组,该元组在structA
成员b
、c
和d
中指定,structB
仅用于将位置存储为 x,y,z 元组。
您有一个处理structB
中的数据的 API,并且由于相同的数据在structA
您面临的问题是,您必须拥有一个由一组重复的函数组成的 API,一个版本的 API 作为参数structB
,另一个版本作为参数structA
。
为了扩展我们在 3D 空间中对象的具体示例,您有一个 API,其中包含一个函数translate()
该函数将坐标平移一定距离。因为根据 MISRA C 有两种不同的结构,所以您需要有两个不同版本的函数,translate_structA()
将参数作为参数structA
,translate_structB()
将参数作为参数structB
。
因此,您必须在API中为每个函数编写两个版本,并且您不想这样做。
备选方案 1 - 用实际结构替换克隆的成员
使用良好的软件工程,而不是在structA
中将此structB
数据类型作为一组克隆的成员,而是将这些克隆的成员替换为structB
。
typedef struct {
int b; // same type of data as in b member of structA above
int c; // same type of data as in c member of structA above
int d; // same type of data as in d member of structA above
} structB;
typedef struct {
int a;
structB xyz; // replace the cloned members with an actual structB
int e;
} structA;
然后,您编写仅在structB
方面与structB
一起使用的 API。在那些使用structA
的地方,您只需在函数调用接口中使用xyz
成员。
这种方法的好处是,如果添加需要structB
的其他新数据类型,您只需加入structB
成员而不是克隆成员,并且使用该structB
的 API 可以与新数据类型一起使用。
但是,为了采用这种方法,您需要拥有技术并能够进行这种更改。另一方面,这是我能想到的最直接,最简单,最易读的替代方案。它还应该具有相当好的运行时效率。
关于接下来两个备选方案的说明
在我进入下两个替代方案之前,您应该考虑这两种选择的基本缺陷。
如果structA
对structB
的依赖没有通过使用structA
中的structB
指定为一种契约,你引入了一种逻辑或认知模块间耦合,其中你有一个公共组件,它是源代码本身,而不是从源代码派生的软件组件。
维护变得很麻烦,因为现在必须同时更改两个结构。除非这两个领域之间的联系记录在源代码和结构定义本身中,否则刚接触代码的程序员可能会错过这一点。
如果引入了使用structB
数据的新数据类型,则需要再次执行克隆步骤,而您只是在扩展复杂链接的表面。
备选方案 2 - 封送到接口对象/从接口对象封送
如果您无法控制结构,那么另一种选择是将数据编送到structA
到structB
中,然后仅根据structB
编写 API。然后,structA
需要使用 API 的任何地方,您都会进行编组或转换,其中挑选出structA
中的特定数据以创建一个临时structB
,然后与函数一起使用。如果函数修改了structB
中的数据,则需要将数据从structB
复制回structA
,然后再消除临时数据。
或者,在您希望对 API 使用structB
的情况下,您可以决定在编组structA
方面执行 API。如果大多数 API 使用structA
而只有少数使用structB
,则此替代方法可能更可取。
有几种方法可以执行这种封送方法,主要取决于 API 接口是否会返回更改的数据对象。
第一种是使用structA
调用一组重复的函数,这组重复的函数处理临时structB
之间的数据封送处理,然后在调用实际 API 时使用
structB
。所以像这样:
int funcThing (structB thing);
int funcThing_structA (structA thing) {
structB temp = {0};
temp.b = thing.b;
temp.c = thing.c;
temp.d = thing.d;
return funcThing (temp);
}
上述方法的替代方案如下:
int funcThing1 (structB thing);
int funcThing2 (structB thing);
int funcThing3 (structB thing);
int funcThingSet_structA (structA thing, int (*f)(structB thing)) {
structB temp = {0};
temp.b = thing.b;
temp.c = thing.c;
temp.d = thing.d;
return f (temp);
}
// and the above is used like
structA thingA;
// … code
i = funcThingSet_structA (thingA, funcThing1); // call funcThing1() with the structA data
i = funcThingSet_structA (thingA, funcThing2); // call funcThing2() with the structA data
i = funcThingSet_structA (thingA, funcThing3); // call funcThing3() with the structA data
如果函数可能会更改数据,则需要确保structA
更新如下:
int funcThing1 (structB *thing);
int funcThing2 (structB *thing);
int funcThing3 (structB *thing);
int funcThingSet_structA (structA *thing, int (*f)(structB *thing)) {
structB temp = {0};
int iRetVal = 0;
temp.b = thing->b;
temp.c = thing->c;
temp.d = thing->d;
iRetVal = f (&temp);
thing->b = temp.b;
thing->c = temp.c;
thing->d = temp.d;
return iRetVal;
}
// and the above is used like
structA thingA;
// … code
i = funcThingSet_structA (&thingA, funcThing1); // call funcThing1() with the structA data
i = funcThingSet_structA (&thingA, funcThing2); // call funcThing2() with the structA data
i = funcThingSet_structA (&thingA, funcThing3); // call funcThing3() with the structA data
您可能还拥有structB
方面的 API,并使用接口帮助程序函数,例如:
structB *AssignAtoB (structB *pB, structA A) {
pB->b = A.b;
pB->c = A.c;
pB->d = A.d;
return pB;
}
structB ConvertAtoB (structA A) {
structB B = {0};
B.b = A.b;
B.c = A.c;
B.d = A.d;
return B;
}
void AssignBtoA (structA *pA, structB B) {
pA->b = B.b;
pA->c = B.c;
pA->d = B.d;
}
然后你可以做这样的事情:
int funcThing1 (structB thing);
int funcThing2 (structB thing);
int funcThing3 (structB thing);
structA aThing;
// …. code
{ // create a local scope for this temporary bThing.
structB bThing = ConvertAtoB (aThing);
i = funcThing1(bThing);
// other operations on bThing and then finally.
AssignBtoA (&aThing, bThing);
}
或者,您的 API 函数可能会返回一个structB
在这种情况下,您可以执行以下操作:
structB funcThing1 (structB thing);
structB funcThing2 (structB thing);
structB funcThing3 (structB thing);
structA aThing;
// …. code
{ // create a local scope for this temporary bThing.
structB bThing = ConvertAtoB (aThing);
bThing = funcThing1(bThing);
bThing = funcThing2(bThing);
AssignBtoA (&aThing, bThing);
}
或
{ // create a local scope for this temporary bThing.
structB bThing = ConvertAtoB (aThing);
AssignBtoA (&aThing, funcThing2(funcThing1(bThing)));
}
甚至只是
AssignBtoA (&aThing, funcThing2(funcThing1(ConvertAtoB (aThing))))
备选方案 3 - 以脆弱的方式使用指针
另一种选择是创建一个指针,其地址以structA
的structB
部分开头。虽然我对MISRA只是模糊地熟悉,但我毫不怀疑这种方法是违反规则的,因为它几乎是一种可憎的东西。然而,无论如何,正如我看到的那样,它是在没有适当软件工程培训的人编写的旧代码中完成的。
使用上述两个结构,创建一个帮助程序函数或宏,该函数或宏将生成指向structB
数据开始structA
偏移量的指针。例如:
structB MakeClone (structA thing) {
return *(structB *)&thing.b; // return a copy of the structB part of structA
}
或
structB *MakePointer (structA *pThing) {
return (structB *)&thing.b; // return a pointer to the structB part of structA
}
预处理器宏也可用于生成第二种情况的指针,如下所示:
#define MAKEPOINTER(pThing) ((structB *)&((pThing)->b))
我还看到了在哪里而不是将辅助函数与赋值一起使用,如下所示:
int funcThing (structB *pBthing);
// then in code want to use the above function with a structA
structA aThing = {0};
// do things with aThing then call our function that wants a structB
funcThing (MAKEPOINTER(&aThing));
相反,他们只会对指针进行硬编码,这使得在维护期间很难找到完成此操作的位置:
funcThing ((structB *)&(aThing.b));
我还看到了使用memcpy()
进行分配的指针方法。因此,如果我们有这样的代码:
structA aThing = {0};
structB bThing = {0};
// somewhere in code we have
memcpy (&bThing, &aThing.b, sizeof(structB)); // assign the structB part of aThing to a structB
// more code to modify bThing then call our function
funcThing (&bThing);
memcpy (&aThing.b, &bThing, sizeof(structB)); // assign the structB back into the structB part of aThing
使用指针方法是脆弱的,因为如果structA
布局或structB
布局应该改变,事情可能会中断。更糟糕的是,它们可能会在没有说明原因和根本原因的情况下破裂。
首先,它建议传递指向结构的指针,而不是按值进行完整复制。
方法1:Linux内核编程中广泛使用的方法container_of
就是宏。如果你给它一个指向元素的指针作为输入,它将为您提供包含该元素的结构。您可以在函数中执行类似操作set_b()
、set_c()
set_d()
。
方法2:以set_b()
为例,可以多加1个参数来说明结构类型,也可以让第一个指针成为void *
。签名将变为:
set_b(void * str, int num, int str_type)
您可以将str_type
用作struct A
的1
,0
用于struct B
。现在在函数定义中,您必须检查类型并将void
指针再次转换为正确的struct
类型