在 C 语言中为相似但不同的数据结构组合通用 API



我有两个结构:A包含a,b,c,d作为成员,B包含b,c,d作为成员。我有多个可以传递AB的 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_ASet_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的频率。您确实没有为明确建议的方法提供足够的信息。

看来您的发布可以重述如下。

有两个结构,structAstructB具有共同的成员。这些公共成员存储相同类型的数据,并且在检查时,整个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成员bcd中指定,structB仅用于将位置存储为 x,y,z 元组。

您有一个处理structB中的数据的 API,并且由于相同的数据在structA您面临的问题是,您必须拥有一个由一组重复的函数组成的 API,一个版本的 API 作为参数structB,另一个版本作为参数structA

为了扩展我们在 3D 空间中对象的具体示例,您有一个 API,其中包含一个函数translate()该函数将坐标平移一定距离。因为根据 MISRA C 有两种不同的结构,所以您需要有两个不同版本的函数,translate_structA()将参数作为参数structAtranslate_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 可以与新数据类型一起使用。

但是,为了采用这种方法,您需要拥有技术并能够进行这种更改。另一方面,这是我能想到的最直接,最简单,最易读的替代方案。它还应该具有相当好的运行时效率。

关于接下来两个备选方案的说明

在我进入下两个替代方案之前,您应该考虑这两种选择的基本缺陷。

如果structAstructB的依赖没有通过使用structA中的structB指定为一种契约,你引入了一种逻辑或认知模块间耦合,其中你有一个公共组件,它是源代码本身,而不是从源代码派生的软件组件。

维护变得很麻烦,因为现在必须同时更改两个结构。除非这两个领域之间的联系记录在源代码和结构定义本身中,否则刚接触代码的程序员可能会错过这一点。

如果引入了使用structB数据的新数据类型,则需要再次执行克隆步骤,而您只是在扩展复杂链接的表面。

备选方案 2 - 封送到接口对象/从接口对象封送

如果您无法控制结构,那么另一种选择是将数据编送到structAstructB中,然后仅根据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 - 以脆弱的方式使用指针

另一种选择是创建一个指针,其地址以structAstructB部分开头。虽然我对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 A10用于struct B。现在在函数定义中,您必须检查类型并将void指针再次转换为正确的struct类型

最新更新