【问题标题】:C - Populate a generic struct inside a function without mallocC - 在没有 malloc 的函数内填充通用结构
【发布时间】:2020-08-20 13:14:54
【问题描述】:

我正在尝试构建一个可以填充结构的通用函数无需任何动态内存分配

以下代码是我正在尝试做的一个简单示例。 此代码不会编译为incomplete type 'void' is not assignable

请注意,这是一个突出我的问题的玩具示例。我真的不想转换颜色;我只是想强调一下,这些结构的数据类型和大小会有所不同。

#include <stdio.h>

typedef struct {
    int r;
    int g;
    int b;
} rgb_t;

typedef struct {
    float c;
    float m;
    float y;
    float k;
} cmyk_t;

typedef enum { RGB, CMYK } color_t;

void convert_hex_to_color(long hex, color_t colorType, void* const out) {
    if (colorType == RGB) {
        rgb_t temp = { 0 };
        // Insert some conversion math here....
        temp.r = 1;
        temp.g = 2;
        temp.b = 3;
        *out = temp; //< [!]
    } else
    if (colorType == CMYK) {
        cmyk_t temp = { 0 };
        // Insert some conversion math here....
        temp.c = 1.0;
        temp.m = 2.0;
        temp.y = 3.0;
        temp.k = 4.0;
        *out = temp; //< [!]
    }
}

int main(void) {
    // Given
    long hex = 348576;
    rgb_t mydata = { 0 };
    convert_hex_to_color(hex, RGB, (void*)(&mydata));

    // Then
    printf("RGB = %i,%i,%i\r\n", mydata.r, mydata.g, mydata.b);
    return 0;
}

对于一些额外的上下文,我在嵌入式系统目标上使用 C11。

最好的[1] 方法是什么?宏?联合?

问候,
加布里埃尔

[1] 我将“最佳”定义为可读性和安全性之间的良好折衷。

【问题讨论】:

  • 可以使用*out = temp; --> memcpy(out, &amp;temp, sizeof temp); 并让编译器针对高效发出的代码进行优化。
  • 注意; convert_hex_to_color(hex, RGB, (void*)(&amp;mydata)); --> C 中不需要强制转换。
  • 你可以替换第一个 'out = temp;'与 '*(rgb_t)out=temp;'等等。只要 void* 实际上是指向正确类型转换为 void* 的指针,这将起作用,
  • @dmuir 我想你想要*(rgb_t*)out=temp;(另一个*
  • @chux 显式转换(to void*)在 C++ 中也不需要。

标签: c generic-programming void-pointers c11


【解决方案1】:

错误的原因是通过void指针存储无效:编译器不知道要存储什么。您可以将指针转换为 *(rgb_t *)out = temp;*(cmyk_t *)out = temp;

或者,您可以将temp 定义为指向适当结构类型的指针,并直接从out 对其进行初始化,而无需进行C 中不需要的强制转换:

void convert_hex_to_color(long hex, color_t colorType, void *out) {
    if (colorType == RGB) {
        rgb_t *temp = out;
        // Insert some conversion math here....
        temp->r = 1;
        temp->g = 2;
        temp->b = 3;
    } else
    if (colorType == CMYK) {
        cmyk_t *temp = out;
        // Insert some conversion math here....
        temp->c = 1.0;
        temp->m = 2.0;
        temp->y = 3.0;
        temp->k = 4.0;
    }
}

请注意,C 中不需要强制转换:

int main(void) {
    // Given
    long hex = 348576;
    rgb_t mydata = { 0 };
    convert_hex_to_color(hex, RGB, &mydata);

    // Then
    printf("RGB = %i,%i,%i\r\n", mydata.r, mydata.g, mydata.b);
    return 0;
}

【讨论】:

  • @bruno:确实好多了,虽然我一个人在办公室,周围有 110 个空座位。 {} 在 C 中作为初始值设定项确实无效。{0} 对所有类型都有效且正常,但有点草率,因为它可能会触发缺少成员初始值设定项的警告。
  • 哦,关于 {0},其他答案也有类似的评论
  • +1 抱歉,我还不能投票。这是一个很好的答案。感谢您的宝贵时间。
【解决方案2】:
rgb_t temp = {0};

这样就在rgb_t 类型的堆栈上声明了一个变量。到目前为止一切顺利,虽然你不需要那个 0

*out = temp;

这是您的问题:在 C 中您只能复制相同类型的内存。曾经。这与malloc 无关,正如您的标题所示,这只是基本的语言规范。当然,有些类型提供隐式转换,但 void* 不是其中之一。

因此,如果您要复制一个结构(右侧的rgb_t),则目标必须是同一类型。所以把这一行改成这样:

*(rgb_t *)out = temp;

【讨论】:

  • 在 C 中,你需要 0 — 写 {} 不是有效的 C,即使某些 C 编译器接受它作为扩展,无疑会受到伴随的 C++ 编译器的影响。
  • 我的立场是正确的,我只真正使用 MSVC 甚至 C++。
【解决方案3】:

“最好”的方法是不要在同一个函数中混合不相关的结构,或者在同一个内存区域中。这只是乱七八糟的设计。

如果您需要为两种不同形式的数据保持一致的 API,那么一个类似类型安全函数的宏可能是一个想法。您可以伪造这样的宏,使其具有类似于通过指针传递数据的语法

void convert_hex_to_color(long hex, type* data)

然后使用 C11 _Generic 来实际确定要使用的正确类型,而不是使用危险的 void 指针。由于您不能“通过引用”将参数传递给宏,因此您必须在其中潜入变量赋值。示例:

#include <stdio.h>

typedef struct {
    int r;
    int g;
    int b;
} rgb_t;

typedef struct {
    float c;
    float m;
    float y;
    float k;
} cmyk_t;



void convert_hex_to_color(long hex, void* data);
  /* 
     Pretty prototype just for code documentation purposes. 
     Never actually defined or called - the actual macro will "mock" this function. 
  */

#define convert_hex_to_color(hex, output) ( *(output) = _Generic(*(output), \
  rgb_t:  (rgb_t){ .r=1, .g=2, .b=3 }, \
  cmyk_t: (cmyk_t){ .c=1.0, .m=2.0, .y=3.0, .k=4.0 } ) )


int main(void) {
    // Given
    long hex = 348576;
    rgb_t  myrgb  = { 0 };
    cmyk_t mycmyk = { 0 };

    convert_hex_to_color(hex, &myrgb);
    convert_hex_to_color(hex, &mycmyk);

    printf("RGB  = %i,%i,%i\r\n", myrgb.r, myrgb.g, myrgb.b);
    printf("CMYK = %f,%f,%f,%f\r\n", mycmyk.c, mycmyk.m, mycmyk.y, mycmyk.k);
    return 0;
}

输出:

RGB  = 1,2,3
CMYK = 1.000000,2.000000,3.000000,4.000000

请注意 _Generic 对类型限定符(const 等)的支持在 C11 中是不稳定的 - 一些 C11 编译器处理 const rgb_trgb_t 不同,其他编译器处理它们相同。这是 C17 中的“错误修复”之一,因此请使用 C17(如果可用)。

【讨论】:

  • 感谢关于 C17 错误修复的说明。我一直很好奇它到底修复了什么。幸运的是,由于 C17 旨在作为一个错误修复而不是一个全新的标准,所以足够新的 gcc 和 clang 版本使 -std=c11 等同于 -std=c17,除了 __STD_C_VERSION__ 设置的值。
  • 我注意到您根据指向的类型而不是指针的类型进行了 _Generic 选择。这样做有什么好处,还是纯粹是一种风格选择?
  • 感谢您的回答。我会注意 C11 与 C17 的问题。
  • @ChristianGibbons 关于 C17,请参阅 Jens Gustedt's blog。我确实相信我能够通过 _Generic 引发错误,而不是在某些编译器上删除类型限定符,但我不记得是哪一个。也就是说,像#define TEST(x) _Generic((x), int const: "foo", int : "bar") 这样的代码编译并保留限定符。在 C17 中,_Generic 的第一个操作数经过“左值转换”并丢弃所有限定符。
  • @ChristianGibbons 关于使用指针,这个例子并不重要,但原因是为具有不同限定符的类型(不管 C11 还是 C17)做一个特例。因为 C 对指针的类型更强,int*const int* 被视为不同的类型。你也可以使用这个技巧来区分数组和指针(指向第一个元素):#define TEST(x) _Generic(&amp;(x), char** : puts("pointer"), char(*)[]: puts("array")).
【解决方案4】:

帧挑战:您似乎想要根据传递给此函数的类型执行不同的操作。而不是使用枚举来告诉它你传入的是什么类型,并基于该枚举进行分支,而是使用 C11 的 _Generic 来处理它,你甚至不需要明确地告诉它类型是什么每次调用:

#include <stdio.h>

typedef struct {
    int r;
    int g;
    int b;
} rgb_t;

typedef struct {
    float c;
    float m;
    float y;
    float k;
} cmyk_t;

inline void convert_hex_to_color_rgb(long hex, rgb_t *const out) {
    (void) hex; // or whatever you're planning to do with 'hex'
    out->r = 1;
    out->g = 2;
    out->b = 3;
}

inline void convert_hex_to_color_cmyk(long hex, cmyk_t *const out) {
    (void) hex; // or whatever you're planning to do with 'hex'
    out->c = 1.0;
    out->m = 2.0;
    out->y = 3.0;
    out->k = 4.0;
}

#define convert_hex_to_color(hex, out) _Generic((out), \
        rgb_t *: convert_hex_to_color_rgb((hex), (rgb_t *)(out)), \
        cmyk_t *: convert_hex_to_color_cmyk((hex), (cmyk_t *)(out)) \
)

int main(void) {
    // Given
    long hex = 348576;
    rgb_t mydata = { 0 };
    cmyk_t mydatac = { 0 };
    convert_hex_to_color(hex, &mydata);
    convert_hex_to_color(hex, &mydatac);

    // Then
    printf("RGB = %i,%i,%i\r\n", mydata.r, mydata.g, mydata.b);
    printf("CMYK = %f,%f,%f,%f\r\n", mydatac.c, mydatac.m, mydatac.y, mydatac.k);
    return 0;
}

【讨论】:

  • 非常有趣。这样你就可以“检测”到我们想要使用的函数的正确版本。这么聪明,你会在生产代码中推荐它吗?有人可能会争辩说,它使功能不那么明确。另外,您知道 _Generic 在程序集调用中的含义吗?再次感谢。
  • 啊,在我发布几乎相同的答案之前没有看到这个答案。我的依赖于宏扩展而不是内联——如果算法本身很复杂,内联肯定更合适,而不仅仅是一堆赋值。
  • 但是,请注意,内联会产生各种类型的限定符打嗝,特别是如果您无缘无故地扔掉type *const API。假设调用者传递了const type*。这对于 C11 中的 _Generic 本身也是有问题的,我在答案中添加了关于类型限定符的注释。
  • @gberth 这可以用于生产代码,但是“函数”的 API 需要在 cmets 中正确记录,就好像它是一个常规函数一样。在头文件中,您甚至可以声明一个具有相同名称 void convert_hex_to_color(long hex, void* ptr) 或类似名称的函数原型,只是为了清楚起见(并使您的 IDE 中的“代码完成”功能满意)。然后让宏“模拟”该函数,就像 C 标准库函数通常用宏而不是实际的函数调用来实现一样。关于程序集,您将看不到 _Generic,它都是编译时的。
  • @Lundin 是的,这些函数看起来很基本,但我的印象是它被缩减为制作 MCVE,因此我认为未使用的 hex 参数将用于实际填充当前使用的结构成员而不是文字赋值。
猜你喜欢
  • 2012-12-15
  • 1970-01-01
  • 1970-01-01
  • 2015-07-02
  • 2018-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-25
相关资源
最近更新 更多