Объединение общего API для похожих, но разных структур данных в C - PullRequest
0 голосов
/ 16 января 2019

У меня есть две структуры: A , содержащие a, b, c, d в качестве членов и B , содержащие b, c, d как участники. У меня есть несколько API, которые могут быть переданы либо A или B .

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);

Какой будет самый простой способ для достижения общих API для того же в C. Например,

Set_b(X, int);
Set_c(X, int);
Set_d(X, int);

Мне не разрешено использовать union, так как код должен соответствовать MISRA C.

Ответы [ 5 ]

0 голосов
/ 19 января 2019

Прежде всего, рекомендуется передавать указатель на структуру, а не полную копию по значению.

Метод 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 как 1 для struct A и 0 для struct B. Теперь внутри определения функции, вы должны проверить тип и снова переустановить указатель void на правильный struct тип

0 голосов
/ 17 января 2019

Существует несколько различных альтернативных подходов, которые могут быть использованы, однако это зависит от того, чем вы владеете, и может отличаться от того, чем вы не владеете, и не может изменять или изменять. Реальная реализация может зависеть от того, как часто structA используется с API против structB. Вы действительно не предоставляете достаточно информации для окончательного предлагаемого подхода.

Похоже, что ваша публикация может быть изменена следующим образом.

Есть две структуры, 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 описывает объект в некотором трехмерном пространстве, местоположение которого представляет собой кортеж x, y, z, который указан в structA членах b, c и d и structB используется для хранения местоположения только в виде кортежа x, y, z.

У вас есть API, который работает с данными в structB, и поскольку те же данные находятся в structA, вы столкнулись с проблемой, что вам нужно иметь API, состоящий из набора функций, который дублируется, один версия API, которая принимает в качестве аргумента structB, а другая принимает в качестве аргумента structA.

Чтобы расширить наш конкретный пример объектов в трехмерном пространстве, у вас есть API, который содержит функцию translate(), которая переводит координаты на некоторое расстояние. Поскольку у вас есть две разные структуры в соответствии с MISRA C, вам понадобятся две разные версии этой функции: translate_structA(), которая принимает в качестве аргумента structA, и вторая translate_structB(), которая принимает в качестве аргумента structB.

Итак, вы столкнулись с необходимостью написать две версии каждой функции в вашем API, и вы не хотите этого делать.

Альтернатива 1 - заменить клонированные элементы действительной структурой

Используйте хорошую программную инженерию и вместо использования этого типа данных structB в structA в качестве клонированного набора элементов, вместо этого замените эти клонированные элементы на 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;

Затем вы пишете API, который работает с structB только в терминах structB. В тех местах, где вы используете structA, вы просто используете член xyz в интерфейсе вызова функции.

Приятной особенностью этого подхода является то, что если добавляются дополнительные новые типы данных, требующие structB, вы просто добавляете элемент structB, а не клонируете элементы, и API, использующий structB, может быть используется с новыми типами данных.

Однако, чтобы воспользоваться этим подходом, вам необходимо владеть технологией и иметь возможность вносить изменения такого рода. С другой стороны, это самая простая, простая и читаемая альтернатива из всех, которые я могу себе представить. Он также должен иметь неплохую эффективность во время выполнения.

Примечание о следующих двух альтернативах

Прежде чем перейти к следующим двум альтернативам, вы должны рассмотреть основной недостаток в обоих из них.

Если зависимость structA от structB не указана как вид контракта с использованием structB в structA, вы вводите логическую или когнитивную межмодульную связь , в которой у вас есть общий компонент, который представляет собой сам исходный код, а не программный компонент, полученный из исходного кода.

Техническое обслуживание становится проблемой, потому что теперь две структуры должны быть изменены вместе. И если связь между этими двумя областями не задокументирована в исходном коде и самих определениях структуры, программист, плохо знакомый с кодом, вероятно, упустит это.

И если вводятся новые типы данных, использующие данные structB, шаг клонирования необходимо повторить, и вы просто расширяете поверхность сложных связей.

Альтернатива 2 - сортировка в / из интерфейсного объекта

Если у вас нет контроля над структурами, то другой альтернативой является выполнение сортировки данных в structA и из *1083* в structB, а затем написать API только в терминах structB. Затем в любом месте, где structA необходимо использовать API, у вас будет сортировка или преобразование, в котором будут выбраны конкретные данные в structA для создания временного structB, который затем используется с функцией. Если функция изменяет данные в structB, вам необходимо скопировать данные из structB обратно в structA, прежде чем удалять временные.

В качестве альтернативы вы можете решить использовать API в терминах structA с маршаллингом в тех случаях, когда вы хотите использовать structB с 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

Вы также можете иметь API в терминах structB и использовать вспомогательные функции интерфейса, такие как:

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 - хрупкое использование указателей

Другой альтернативой является создание указателя, адрес которого начинается с части structB в structA. Хотя я только смутно знаком с MISRA, я почти не сомневаюсь, что этот подход противоречит правилам, поскольку он в значительной степени мерзок. Тем не менее, здесь все равно, как я видел, это было сделано в старом коде, написанном людьми без надлежащей подготовки программного обеспечения.

Используя две вышеупомянутые структуры, создайте вспомогательную функцию или макрос, который сгенерирует указатель на смещение в structA, где начинаются данные structB. Например:

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 layout или structB layout должны измениться, вещи, вероятно, сломаются. Хуже всего то, что они могут сломаться без указания причины и основной причины.

0 голосов
/ 16 января 2019

Я бы сделал это так. Это отладка и не генерирует слишком много кода (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("%d\n", x.a);
    printf("%d\n", y.a);
}
0 голосов
/ 16 января 2019

Вы можете создать интерфейс. Например,

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);

сделать то же самое для других структур

0 голосов
/ 16 января 2019

C11 поддерживает типовые выражения, которые должны делать то, что вы ищете:

#define Set_b(X, y) _Generic((X), A: Set_b_A,\
                                  B: Set_b_B \
                            )((X), (y))

Тогда просто реализуйте Set_b_A и Set_b_B для соответствующих типов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...