【问题标题】:Practical way of implementing comparison of unions in c在c中实现联合比较的实用方法
【发布时间】:2021-09-17 14:49:38
【问题描述】:

出于某些测试目的,我需要比较两个联合以查看它们是否相同。将所有成员一一比较就够了吗?

union Data {
    int i;
    float f;
    char* str;
} Data;

bool isSame(union Data left, union Data right)
{
    return (left.i == right.i) && (left.f == right.f) && (left.str == right.str);
}

我的预感是,如果一个工会首先包含一个较大的类型,然后切换到一个较小的类型,它可能会失败。我看到一些建议提到将联合包装在一个结构中(比如这里:What is the correct way to check equality between instances of a union?),它跟踪联合当前是哪种数据类型,但我不知道这实际上是如何实现的。我不需要在设置联合值的每个实例中手动设置联合类型吗?

struct myData
{
    int dataType;
    union {
        ...
    } u;
}

void someFunc()
{
    struct myData my_data_value = {0};
    my_data_value.u.i = 5;
    my_data_value.u.dataType = ENUM_TYPE_INTEGER;

    my_data_value.u.f = 5.34;
    my_data_value.u.dataType = ENUM_TYPE_FLOAT;
    
    ...
}

似乎没有必要将涉及我的联合的所有代码加倍,以便能够对联合值进行完美比较。我是否错过了一些明显的聪明方法来解决这个问题?

【问题讨论】:

  • 您想通过比较得出什么结论?某些元素的平等?然后比较它们。占用内存是否相等?然后使用memcmp
  • 我认为你最好编写一个函数来比较myDatas 而不是us。如果dataType 不同,您可以从提前退出开始。然后,您可以打开dataType 并比较有效的联合成员。我认为这可能是安全性和性能之间的最佳平衡。我已经很久没有使用联合了,但是......如果你不跟踪哪个成员是有效的,那么你可能会遇到比比较其中两个更大的问题。
  • @EugeneSh。如果我通过程序的某些部分推送一些一般数据,我知道它会以联合形式结束(例如 union myData finalData = pushThrough("1234.09")),我想编写一个类似的测试:assert(isSame(1234.09 , finalData) == true),但如果我能概括一下是否在左侧也采用联合,那就太好了,所以我不必对联合中的每种类型进行比较 fcn(例如 isSameFloat( ...),isSameDouble()...,isSameInt()...)。是不是更清楚了?
  • @TimRandall 您是说必须始终将联合包装在结构中吗?我使用联合来包含可能是不同类型的数据,主要是作为一种简化/概括我的代码的方式(所以我不需要为我使用的每种类型创建不同的函数)。当我需要转换数据时,我“知道”数据是什么类型,这样我就可以使用正确的成员,但介于两者之间的只是一些“通用”数据,由联合符号表示。我不知道这是否“正确”,但我发现那一刻我的生活变得更简单了。
  • 为了正确使用联合(),您需要知道哪个元素是当前活动的元素。你怎么知道的并不重要。您可以将当前元素 ID 保存在一个结构中,或者根据其他数据即时计算它,或者其他任何东西,但是您需要以某种方式在某个地方拥有这些信息。否则,联合只是一堆毫无意义的比特。 () “正确”是指“没有类型双关语”。类型双关语在技术上是允许的,但它确实把你的联合变成了一堆毫无意义的东西。如果这是您想要的,请与 memcmp 进行比较。

标签: c unions ansi-c


【解决方案1】:

如果您的建议有效,那么您可以通过使用memcmp(&left, &right, sizeof left) 获得相同的效果而无需多次比较。但这行不通,你的提议也行不通,出于同样的原因。

首先,分配给不占用分配给联合的所有字节的联合成员对未占用的字节具有未指定的影响。最有可能的是,它们不会从以前的值修改,但任何值都是可能的。比较这些字节的值有一个未指定的结果。

您可能认为在分配成员之前将联合的字节设置为 0 会允许比较工作,但标准不要求未修改未使用的字节。此外,许多编译器会优化清除联合的尝试,理由是如果下一条语句为联合赋予新值则没有法律效力。

尝试比较非当前值的联合成员还有其他原因,即使这两个值都不包含填充也是如此。

例如,如果您不知道两个联合值当前具有相同的活动成员,则可能会得到错误的等价。 (每个float 与一些int 具有相同的位模式,但两个值肯定不相同。)

不太明显的是,具有不同位模式的两个值实际上可能相等。 (例如,浮点 0.0 和 -0.0 被认为是相等的。)

最后,不是每个位模式都是有效的float;如果一个或两个联合值是 int,其位模式对应于浮动 NaN,则尝试将值与 floats 进行比较肯定会产生错误的答案(NaN 不等于自身)并可能抛出浮点异常。

简而言之,如果您不知道哪个类型对联合有效,则无法有效地使用联合值,只能将其分配给同一联合类型的另一个对象。这意味着必须有某种机制(内部或外部)来标识联合的活动类型。

外部机制(例如,在 yacc 生成的解析器中使用)和内部机制(所谓的“可区分联合”,正如您在问题末尾所建议的那样)之间的选择将取决于精确的应用程序环境。

【讨论】:

  • (Actually, there's an additional reason your proposal won't work: accessing a union member other than the last one used to assign the union is Undefined Behaviour. 在 C++ 中而不是在 C 中
  • @0___________:是的,如果位模式不是用于访问它的类型的值,它只是 UB。所以我应该说“可能是未定义的行为”,但经过反思,删除括号观察似乎更容易,因为它并不真正相关。
【解决方案2】:

union 的棘手之处在于无法判断哪个成员是“活跃的”成员。如果所有成员的大小恰好相同,则只需检查一个成员即可。

如果成员的大小不同,您可能会遇到一些陷阱。

如果您设置一个较大的成员后跟一个较小的成员,则较大成员使用的额外字节将保持不变。例如:

union u1 {
    unsigned int a;
    unsigned short b;
};
union u1 x,y;
x.a = 0x12345678;
y.a = 0x87654321;
x.b = 0;
y.b = 0;

逻辑上xy 具有相同的值,但x.a == y.a 为假,memcmp(&x, &y, sizeof x) 将返回非零值。

只设置较小的值可能会更糟:

union u1 x,y;
x.b = 0;
y.b = 0;

由于额外的字节具有不确定的值,并且执行x.a == y.a 将通过尝试读取这些值来触发undefined behavior

您需要以某种方式跟踪活动成员,才能知道要阅读哪一个。执行此操作的最简单方法是将union 包装在带有“标签”字段的struct 中,以便您知道要检查哪个。

您在 cmets 中提到的 isSame 函数必须采用包含 struct 的两个实例,并使用 switch 语句来选择要检查的字段。当您调用它时,您可以使用复合文字来创建结构的临时副本以进行比较,即:

isSame((struct myData){ .datatype = ENUM_TYPE_INTEGER, .u = { .i= 5 }}, finalData)
isSame((struct myData){ .datatype = ENUM_TYPE_FLOAT, .u = { .f= 5.34 }}, finalData)

【讨论】:

    【解决方案3】:

    将所有成员一一比较就够了吗?

    取决于您的比较方式。
    考虑:

    union Data {
        float f;
    } Data;
    
    if (a.f == b.f) ....
    

    a.f 为+0.0 且b.f 为-0.0 时,a.f == b.f 为真。
    a.fb.f 为 NaN 时,a.f == b.f 为假,即使使用相同的位模式。

    最好使用memcmp()


    似乎没有必要将涉及我的联合的所有代码加倍,只是为了能够对联合值进行完美比较。

    这是测试代码。不要担心所有代码都加倍。进行完美的比较。


    我是否错过了一些明显的聪明方法来解决这个问题?

    将最宽的类型与memcmp() 进行比较就足够了。

    目前尚不清楚 OP 想要如何处理更窄领域中的剩余垃圾。 IMO,memcmp() 比较只应与.dataType 所示分配的最后一个成员发生。

    【讨论】:

      【解决方案4】:

      我要做的是将最大的元素相互比较(当有floatint 时),或者当有指针类型时(string),比较指针的元素,所以他有这样的东西:

      bool isSame(struct myData d1, struct myData d2)
      {
          if(d1.dataType != d2.dataType)
              return 0; // invalid comparison
          if(d1.dataType == ENUM_TYPE_STRING)
              return !strcmp(d1.str, d2.str); // <--- compare the strings
          return d1.str == d2.str; // <--- compare either int/int or float/float
      }
      

      这将根据数据类型比较两个字符串或数字。

      我说d1.str;我只是将union 放入struct 作为unnamed union,这样您就可以从myData struct 访问变量:

      struct myData
      {
          int dataType;
          union {
              int i;
              float f;
              char *str;
          };
      };
      

      现在,我必须说我还没有完全在这里回答你的问题……

      您必须跟踪数据类型。如果您查看上面的 isSame 函数,我们不能将无效数据相互比较,例如字符串指针和浮点数,这没有意义。即使在上面使用memcmp,它也不会比较该字符串指针处的字节,而是比较指针值本身。

      因此,您必须跟踪数据类型并将数据联合包装到另一个结构中。

      【讨论】:

        【解决方案5】:

        如果您不知道在最后一次分配给两个变量时使用了哪个选择器,则无法比较两个 unions。比较两个联合(正确)的唯一方法是使用相同的选择器字段分配它们,并且使用该选择器类型可用的比较具有相同的值。

        假设你有:

        union data {
            int i;
            float f;
        };
        

        你有两个变量AB,它们是这样分配的:

        A.i = 0x80000000; /* the integer value -2147483648 */
        B.f = 0.0; /* the float value 0.0 */
        

        第一个,使用float选择器,使用IEEE-722二进制浮点表示,它们可以与truefalse进行比较,因为A.i被重新解释为float-0.0,这如果比较A.f == B.f,则匹配等于B.f(如浮点-0.0 == +0.0)。但是,如果您比较 A.i == B.i,它们将与 false 进行比较,(A.i 应该是 -2147483648,而 B.i 应该是 0)二进制图像确实不同。所以您需要知道上次分配中使用了哪个字段选择器。

        另外,假设你有:

        union data {
            char c;
            char s[100];
        }
        

        假设您像以前一样拥有变量AB,它们已被分配:

            strcpy(A.s, "hello, kitty");
            strcpy(B.s, "hello, world");
        

        如果它们与A.c == B.c 比较,它们将比较为true,因为两个字符串中的第一个字符相同。但是false如果与strcmp(A.s, B.s) == 0比较(在这种情况下这应该是比较它们的正确方法)

        更多,因为如果我们稍后使用A.c = 'H';B.c = 'H';,那么如果我们使用A.c == B.c,它们将比较true(这现在应该是正确的比较方法),而如果我们使用false,它们将比较false我们使用strcmp(A.s, B.s) == 0。无论如何,第二个选择器(char [100] 类型的选择器)可以作为字符数组(按字典顺序,或任何其他排序规则)或字符串(以空分隔)进行比较,给出不同的结果,具体取决于它们的分配历史有过。

        最后,假设你有:

        union data {
            struct {
                char a1; /* compiler should pad 3 byte space before next field */
                int[100] a2; /* compiler can pad 4 byte space space before next field */
                double[23] a3;
            } a;
            struct {
                double[12] b1; 
                char b2; /* compiler could pad 3 bytes space before next field */
                int b3[100];
            } b;
        };
        

        那我们应该如何比较呢? (认为​​一个选择器中的填充孔可以是另一个选择器中的有效数据。)

        【讨论】:

          猜你喜欢
          • 2013-01-21
          • 1970-01-01
          • 2016-05-31
          • 2017-02-25
          • 2011-08-12
          • 1970-01-01
          • 2020-12-19
          • 2013-09-16
          • 2010-11-14
          相关资源
          最近更新 更多