实例特定的编译时代码生成

问题描述 投票:0回答:0

我想出了两种不同的方法来在 C 中实现通用算法和数据结构。我需要你的帮助来决定哪个更好。

过去几周我一直在两者之间权衡,试图决定哪种方法更适合在公司环境中使用。哪种方法产生更高的代码质量、更容易调试、更适合未来、更易读、更可维护并且更容易让新手掌握。更好的方法不一定在上述所有品质上都表现出色。

下面我将通过代码示例和我观察到的一些优点和缺点来展示这两个实现。这些示例只是实际通用数据结构所包含内容的一小部分。

对于脑力练习,请尝试想象这两种方法将如何使用逐元素复制或标准 memcpy() 来实现从 FIFO/队列中添加和获取多个元素。 FIFO 将只实现一组功能:

FIFO_AddMultiple()
FIFO_TakeMultiple()
,而队列必须实现两组功能:
Queue_PushMultipleElementWise()
Queue_PushMultipleMemoryWise()
Queue_PopMultipleElementWise()
Queue_PopMultipleMemoryWise()
.

#1 实例特定的编译时代码生成

FIFO.h

#if !defined(FIFO_H_)
#define FIFO_H_

#include <stdbool.h>
#include <stddef.h>

#define ARRAY_SIZE(array)       (sizeof(array) / sizeof(array[0]))

#define PREPROC_PASTE_TWO(_1, _2)   PREPROC_PASTE_TWO_(_1, _2)
#define PREPROC_PASTE_TWO_(_1, _2)  _1##_2

#define PREPROC_PASTE_THREE(_1, _2, _3)     PREPROC_PASTE_THREE_(_1, _2, _3)
#define PREPROC_PASTE_THREE_(_1, _2, _3)    _1##_2##_3

/**
 * FIFO control.
 */
typedef struct FIFO_Control
{
    size_t Head;        /**< Head index. */
    size_t Tail;        /**< Tail index. */
    size_t NofAdds;     /**< Total number of elements added to FIFO. */
    size_t NofTakes;    /**< Total number of elements taken from FIFO. */
} FIFO_Control_s;

#define FIFO_t(tag)     struct PREPROC_PASTE_TWO(FIFO_, tag)

#define FIFO_STATIC_DEFINE(handle)  {.Control = {0}, .Size = ARRAY_SIZE((handle).Buffer)}

#define FIFO_Occupied(tag, pHandle)         PREPROC_PASTE_THREE(FIFO_, tag, _Occupied)(pHandle)
#define FIFO_Free(tag, pHandle)             PREPROC_PASTE_THREE(FIFO_, tag, _Free)(pHandle)
#define FIFO_IsFull(tag, pHandle)           PREPROC_PASTE_THREE(FIFO_, tag, _IsFull)(pHandle)
#define FIFO_IsEmpty(tag, pHandle)          PREPROC_PASTE_THREE(FIFO_, tag, _IsEmpty)(pHandle)
#define FIFO_Add(tag, pHandle, pElement)    PREPROC_PASTE_THREE(FIFO_, tag, _Add)(pHandle, pElement)
#define FIFO_Take(tag, pHandle, pElement)   PREPROC_PASTE_THREE(FIFO_, tag, _Take)(pHandle, pElement)
#define FIFO_Seek(tag, pHandle, offset)     PREPROC_PASTE_THREE(FIFO_, tag, _Seek)(pHandle, offset)    
#define FIFO_Peek(tag, pHandle, offset)     PREPROC_PASTE_THREE(FIFO_, tag, _Peek)(pHandle, offset)

static inline void FIFO_AdvanceIndex(size_t *const pIndex, const size_t size, const size_t n)
{
    *pIndex = (*pIndex < (size - n)) ? *pIndex + n : *pIndex - (size - n);
}

static inline void FIFO_AdvanceHead(FIFO_Control_s *const pControl, const size_t size, const size_t n)
{
    FIFO_AdvanceIndex(&pControl->Head, size, n);
    pControl->NofAdds += n;
}

static inline void FIFO_AdvanceTail(FIFO_Control_s *const pControl, const size_t size, const size_t n)
{
    FIFO_AdvanceIndex(&pControl->Tail, size, n);
    pControl->NofTakes += n;
}

static inline size_t FIFO_Head(const FIFO_Control_s *const pControl)
{
    return pControl->Head;
}

static inline size_t FIFO_Tail(const FIFO_Control_s *const pControl)
{
    return pControl->Tail;
}

static inline size_t FIFO_NofOccupied(const FIFO_Control_s *const pControl)
{
    return (pControl->NofAdds - pControl->NofTakes);
}

static inline size_t FIFO_NofFree(const FIFO_Control_s *const pControl, const size_t size)
{
    return (size - FIFO_NofOccupied(pControl));
}

static inline bool FIFO_IsBufferFull(const FIFO_Control_s *const pControl, const size_t size)
{
    return (FIFO_NofOccupied(pControl) == size);
}

static inline bool FIFO_IsBufferEmpty(const FIFO_Control_s *const pControl)
{
    return (FIFO_NofOccupied(pControl) == 0);
}

#endif /* FIFO_H_ */

/* Instance specific compile-time code generation from here to end of file. */

#if !defined(FIFO_CONFIG_ADD_LOCK) && !defined(FIFO_CONFIG_ADD_UNLOCK)
    #define FIFO_CONFIG_ADD_LOCK()      (void)0
    #define FIFO_CONFIG_ADD_UNLOCK()    (void)0
#endif

#if !defined(FIFO_CONFIG_TAKE_LOCK) && !defined(FIFO_CONFIG_TAKE_UNLOCK)
    #define FIFO_CONFIG_TAKE_LOCK()     (void)0
    #define FIFO_CONFIG_TAKE_UNLOCK()   (void)0
#endif

/**
 * Instance specific FIFO.
 */
FIFO_t(FIFO_CONFIG_TAG)
{
    FIFO_Control_s Control;                     /**< FIFO control. */
    const size_t Size;                          /**< Buffer size. */
    FIFO_CONFIG_TYPE Buffer[FIFO_CONFIG_SIZE];  /**< Statically allocated buffer. */
};

static inline size_t FIFO_Occupied(FIFO_CONFIG_TAG, const FIFO_t(FIFO_CONFIG_TAG) *const pHandle)
{
    return FIFO_NofOccupied(&pHandle->Control);
}

static inline size_t FIFO_Free(FIFO_CONFIG_TAG, const FIFO_t(FIFO_CONFIG_TAG) *const pHandle)
{
    return FIFO_NofFree(&pHandle->Control, pHandle->Size);
}

static inline bool FIFO_IsFull(FIFO_CONFIG_TAG, const FIFO_t(FIFO_CONFIG_TAG) *const pHandle)
{
    return FIFO_IsBufferFull(&pHandle->Control, pHandle->Size);
}

static inline bool FIFO_IsEmpty(FIFO_CONFIG_TAG, const FIFO_t(FIFO_CONFIG_TAG) *const pHandle)
{
    return FIFO_IsBufferEmpty(&pHandle->Control);
}

static bool FIFO_Add(FIFO_CONFIG_TAG, FIFO_t(FIFO_CONFIG_TAG) *const pHandle, const FIFO_CONFIG_TYPE *const pElement)
{
#if !defined(FIFO_CONFIG_OVERWRITE_FULL)
    if (FIFO_IsFull(FIFO_CONFIG_TAG, pHandle))
    {
        return false;
    }
#endif

    FIFO_CONFIG_ADD_LOCK();

    pHandle->Buffer[FIFO_Head(&pHandle->Control)] = *pElement;
    FIFO_AdvanceHead(&pHandle->Control, pHandle->Size, 1);

    FIFO_CONFIG_ADD_UNLOCK();

    return true;
}

static bool FIFO_Take(FIFO_CONFIG_TAG, FIFO_t(FIFO_CONFIG_TAG) *const pHandle, FIFO_CONFIG_TYPE *const pElement)
{
    if (FIFO_IsEmpty(FIFO_CONFIG_TAG, pHandle))
    {
        return false;
    }

    FIFO_CONFIG_TAKE_LOCK();

    *pElement = pHandle->Buffer[FIFO_Tail(&pHandle->Control)];
    FIFO_AdvanceTail(&pHandle->Control, pHandle->Size, 1);

    FIFO_CONFIG_TAKE_UNLOCK();

    return true;
}

static void FIFO_Seek(FIFO_CONFIG_TAG, FIFO_t(FIFO_CONFIG_TAG) *const pHandle, const size_t offset)
{
    FIFO_CONFIG_TAKE_LOCK();

    FIFO_AdvanceTail(&pHandle->Control, pHandle->Size, offset);

    FIFO_CONFIG_TAKE_UNLOCK();
}

static FIFO_CONFIG_TYPE * FIFO_Peek(FIFO_CONFIG_TAG, const FIFO_t(FIFO_CONFIG_TAG) *const pHandle, const size_t offset)
{
    size_t index = FIFO_Tail(&pHandle->Control);
    FIFO_AdvanceIndex(&index, pHandle->Size, offset);

    return &pHandle->Buffer[index];
}

#undef FIFO_CONFIG_TAG
#undef FIFO_CONFIG_TYPE
#undef FIFO_CONFIG_SIZE
#undef FIFO_CONFIG_OVERWRITE_FULL
#undef FIFO_CONFIG_ADD_LOCK
#undef FIFO_CONFIG_ADD_UNLOCK
#undef FIFO_CONFIG_TAKE_LOCK
#undef FIFO_CONFIG_TAKE_UNLOCK

main.c

#include <stdio.h>

#define FIFO_CONFIG_TAG     MyFIFO
#define FIFO_CONFIG_TYPE    int
#define FIFO_CONFIG_SIZE    10
#include "FIFO.h"

static FIFO_t(MyFIFO) gFIFO = FIFO_STATIC_DEFINE(gFIFO);
static FIFO_t(MyFIFO) gFIFO2 = FIFO_STATIC_DEFINE(gFIFO2); // Same FIFO instance

// By reconfiguring and reincluding FIFO.h another different FIFO instance can be created
#define FIFO_CONFIG_TAG     OtherFIFO
#define FIFO_CONFIG_TYPE    float
#define FIFO_CONFIG_SIZE    3
#include "FIFO.h"  

static FIFO_t(OtherFIFO) gFIFO3 = FIFO_STATIC_DEFINE(gFIFO3);

int main(void)
{
    for (int i = 0; i < 10; i++)
    {
        FIFO_Add(MyFIFO, &gFIFO, &i);
    }
    
    int x;
    while (FIFO_Take(MyFIFO, &gFIFO, &x))
    {
        printf("%d\n", x);
    }

    FIFO_Add(OtherFIFO, &gFIFO3, &x);
    FIFO_Take(OtherFIFO, &gFIFO3, &x);
    printf("%d\n", x);
    
    return 0;
}

好处

  • 使用函数而不是宏提供类型安全
  • 因为使用函数而不是宏意味着我们可以干净地返回和传递变量
  • 不需要在调用位置内联的函数,这减少了整体代码大小
  • 使用宏轻松集成任何用户提供的功能(参见 FIFO_CONFIG_ADD_LOCK、FIFO_CONFIG_ADD_UNLOCK 示例)
  • 与特定配置不相关的代码可以#ifdef 退出编译

弱点

  • 调试有些笨拙,因为在函数内部放置断点会导致断点被放置在随机选择的生成函数之一或所有函数中(这是我的猜测)
  • 需要在FIFO中传递的函数Tag(或者调用tagged函数)
  • 非常规(?我没见过有人用过这个)感觉有点格格不入的C编码实践
  • 需要为每个实例包含 FIFO 标头(非常规的 C 编码实践)
  • 宏观魔术很难遵循

#2 类函数宏代码实现

Queue.h

#if !defined(QUEUE_H_)
#define QUEUE_H_

#include <stdbool.h>
#include <stddef.h>

#define ARRAY_SIZE(array)       (sizeof(array) / sizeof(array[0]))

/**
 * Queue control structure.
 */
typedef struct Queue_Control
{
    size_t Head;        /**< Head index. */
    size_t Tail;        /**< Tail index. */
    size_t TotalPushed; /**< Total number of elements pushed to queue. */
    size_t TotalPopped; /**< Total number of elements popped from queue. */
} Queue_Control_s;

/**
 * Declare statically allocated queue.
 */
#define QUEUE_DECLARE_STATIC(type, size)                                        \
struct                                                                          \
{                                                                               \
    Queue_Control_s Control;    /**< Queue control. */                          \
    const size_t Size;          /**< Queue size (number of elements). */        \
    type Buffer[size];          /**< Statically allocated element buffer. */    \
}

/**
 * Define statically allocated queue.
 */
#define QUEUE_DEFINE_STATIC(handle) {.Control = {0}, .Size = ARRAY_SIZE((handle).Buffer)}

inline static size_t Queue_Generic_AdvanceIndex(const size_t index, const size_t size, const size_t n)
{
    return ((index < (size - n)) ? index + n : index - (size - n));
}

inline static void Queue_Generic_AdvanceHead(Queue_Control_s *const pControl, const size_t size, const size_t n)
{
    pControl->Head = Queue_Generic_AdvanceIndex(pControl->Head, size, n);
    pControl->TotalPushed += n;
}

inline static void Queue_Generic_AdvanceTail(Queue_Control_s *const pControl, const size_t size, const size_t n)
{
    pControl->Tail = Queue_Generic_AdvanceIndex(pControl->Tail, size, n);
    pControl->TotalPopped += n;
}

inline static size_t Queue_Generic_Count(const Queue_Control_s control)
{
    return (control.TotalPushed - control.TotalPopped);
}

inline static size_t Queue_Generic_Free(const Queue_Control_s control, const size_t size)
{
    return (size - Queue_Generic_Count(control));
}

inline static bool Queue_Generic_IsFull(const Queue_Control_s control, const size_t size)
{
    return (Queue_Generic_Count(control) == size);
}

inline static bool Queue_Generic_IsEmpty(const Queue_Control_s control)
{
    return (Queue_Generic_Count(control) == 0);
}

#define Queue_Count(handle)      Queue_Generic_Count((handle).Control)

#define Queue_Free(handle)       Queue_Generic_Free((handle).Control, (handle).Size)

#define Queue_IsFull(handle)     Queue_Generic_IsFull((handle).Control, (handle).Size)

#define Queue_IsEmpty(handle)    Queue_Generic_IsEmpty((handle).Control)

#define Queue_Push(handle, element)                                     \
    do                                                                  \
    {                                                                   \
        (handle).Buffer[(handle).Control.Head] = (element);             \
        Queue_Generic_AdvanceHead(&(handle).Control, (handle).Size, 1); \
    } while(0)

#define Queue_Pop(handle, pElement)                                     \
    do                                                                  \
    {                                                                   \
        *(pElement) = (handle).Buffer[(handle).Control.Tail];           \
        Queue_Generic_AdvanceTail(&(handle).Control, (handle).Size, 1);  \
    } while (0)

#define Queue_Seek(handle, offset) Queue_Generic_AdvanceTail(&(handle).Control, (handle).Size, offset)

#define Queue_Peek(handle, offset)  (&(handle).Buffer[Queue_Generic_AdvanceIndex(Queue_Generic_Tail(handle), size, offset)])

#endif /* QUEUE_H_ */

main.c

#include <stdio.h>
static QUEUE_DECLARE_STATIC(int, 10) gQueue = QUEUE_DEFINE_STATIC(gQueue);
static QUEUE_DECLARE_STATIC(float, 3) gQueue2 = QUEUE_DEFINE_STATIC(gQueue2);

int main(void)
{
    for (int i = 0; i < 10; i++)
    {
        Queue_Push(gQueue, i);
    }

    int x;
    while (!Queue_IsEmpty(gQueue))
    {
        Queue_Pop(gQueue, &x);
        printf("%d\n", x);
    }
    return 0;
}

好处

  • 无论单个源文件中使用了多少个队列实例,Header 仅包含一次(常规 C 编码实践)
  • 与#1 方法相比更传统的 C 代码
  • 直接传递到类似函数的宏队列句柄(无指针)
  • 所有类型队列的单一函数定义(不需要Tag
  • 队列配置(类型,大小)在队列的声明中所以不需要
    #define
    配置并包含标题

弱点

  • 会导致长宏
  • 调试宏可能很痛苦
  • 因为使用宏而不是函数,它们有时需要依赖
    do{} while(0)
    和逗号运算符构造,它们有其缺点,尤其是在返回值时(这对于函数来说真的很容易)
  • 宏总是在调用处内联,因此代码大小可能会爆炸
  • 与宏相关的编译器警告和错误有时会显得不连贯且难以理解
  • 不同实例配置的所有可能函数必须由头文件提供,即使特定实例不使用它,这意味着必须提供多个类似函数的宏(具有不同名称)以不同方式实现某些功能(添加/获取本文开头描述的多个元素)
  • 根据配置,无法轻松访问用户提供的功能(如前面示例中的线程锁定和解锁)

你会使用哪一个,为什么?

我倾向于#1,但也担心如果我们的代码库大量使用方法#1,那么新员工将不容易掌握和维护代码。此外,更重要的是,我不确定方法 #1 如何与测试、静态分析和汽车标准集成并很好地发挥作用。

欢迎对任何一种方法进行任何改进、更改或评论。

c algorithm generics data-structures c-preprocessor
© www.soinside.com 2019 - 2024. All rights reserved.