【问题标题】:C++ I think my union may be producing undefined behaviourC++ 我认为我的工会可能会产生未定义的行为
【发布时间】:2018-12-26 10:51:18
【问题描述】:

我使用联合来表示 rgb 像素数据,因此可以将其作为uint8_t 的连续数组或作为单个 rgb 元素进行访问。 (我认为这可能是工会的少数用途之一。)

类似这样的:

union PixelRGB
{
    uint8_t array[3];
    struct rgb
    {
        uint8_t b;
        uint8_t g;
        uint8_t r;
    };
};

我实际上是从网上的其他地方找到这段代码的,这表明这是一种潜在的用途。

几分钟前我问了一个关于另一个问题的问题,但被告知我的工会可能会产生 UB。

我已经阅读了this,它似乎表明如果我从一个不是最后一个要写入的成员中读取,那么结果是 UB...

我的问题是为什么?

我的假设(也许是错误的?)是在内存中联合将被表示为:

b = array[0]
g = array[1]
r = array[2]

即;这些变量中的每一个都在内存中占用完全相同的字节(空间/位置/地址),因此我假设该行

g = 0xff;

会将array[1] 的值更改为0xff。

我错了吗?为什么?

为了论证:C++ 14

重复的问题不是重复的:链接的问题询问联合内的两个结构是否是 UB - 不是我在这里问的。

【问题讨论】:

  • 我今天已经多次看到这个确切的结构。这个问题与您的其他问题有何不同?
  • 而 C++ 不允许 type-punning 使用联合。
  • 这里的有趣点:你声明了一个类型,嵌套在联合体中,但联合体不包含该声明类型的子对象!

标签: c++ undefined-behavior unions


【解决方案1】:

我已经通读了这个,它似乎确实表明如果我从一个不是最后一个要写入的成员中读取,那么结果是 UB...

我的问题是为什么?

因为标准是这么说的1。该标准有时会强加给我们(C++ 开发人员)的规则,这些规则允许实现(编译器)忽略边缘情况并允许他们针对名义情况进行优化。就是这样的规矩。

某些编译器可能会构建一个按您预期工作的二进制文件。有些可能会产生崩溃的可执行文件。有些人可能会做任何介于两者之间或看似随机的事情。 未定义的行为是未定义的2。


1)

[class.union]/1

在联合中,如果非静态数据成员的名称引用其生命周期已经开始但尚未结束的对象 ([basic.life]),则它是活动的。 union 类型的对象的非静态数据成员在任何时候最多可以有一个是活动的,即任何时候最多可以将一个非静态数据成员的值存储在一个 union 中。

和

[basic.life]/7

类似地,在对象的生命周期开始之前但在对象将占用的存储空间分配之后,或者在对象的生命周期结束之后并且在对象占用的存储空间被重用或释放之前,任何glvalue可以使用指代原始对象的那个,但只能以有限的方式使用。 [...] 如果:

程序有未定义的行为
  • glvalue 用于访问对象,或
  • [...]

为了便于人类解析,使用以下上下文:

union { unsigned a; char b[sizeof(unsigned)]; } u;
u.a = 0;       // (1)
(void) u.b[0]; // (2) UB

在线标记为(1),u.a 现在是u 的活跃成员。根据[class.union]/1,由于联合中只有一个非静态成员可以同时处于活动状态,u.b不处于活动状态。

这意味着在标记为(2) 的行上,我们访问一个对象的值,该对象的生命周期尚未开始或已经结束,其方式使其行为未定义[basic.life]/7。


2)

[defns.undefined]

未定义的行为
本文档没有要求的行为

【讨论】:

  • 相反,[basic.lval]/11:“如果程序尝试通过以下类型之一的左值访问对象的存储值,则行为未定义:[...] @987654325 @ -- 一种聚合或联合类型,在其元素或非静态数据成员中包括上述类型之一(递归地包括子聚合或包含联合的元素或非静态数据成员)”。明显缺少访问联合使用匹配访问类型的成员的要求。
  • ... 此外,我不清楚 [basic.life]/7 在这里如何应用,因为有问题的对象是 聚合或联合,而不是它的活动成员,我不知道标准中规定访问,无论是有效还是无效,都应该结束一个对象的生命周期。更进一步,[class.union]/1 立即将“活动成员”定义为指代在联合中存储的数据类型。没有提到可以访问哪些类型的数据,因此返回到 [basic.lval]/11。
  • 我在写这篇评论的时候确实读了好几遍。关于哪个对象是“活的”的细节,我确实是不正确的。但是,也许您可​​以引用这样的措辞,即访问联合的非活动成员会结束活动成员的生命周期?显然,这种访问不能在所述生命周期之前进行,否则所述成员将不会处于活动状态。在没有这样的措辞的情况下,显然 [basic.life]/7 不能适用,因为它明确指的是分配和生命周期开始之间的时期,以及生命周期结束和重用/释放之间的时期。跨度>
  • 此外,[basic.lval]/6 似乎适用于通过联合通过 access 进行的类型双关:“每当一个泛左值作为操作数出现时,它期望一个纯右值该操作数、左值到右值、数组到指针或函数到指针的标准转换被应用于将表达式转换为纯右值。” ...
  • ... 因此,我们关注[[conv.lval],尤其是(3.1),因为所讨论的访问似乎不违反本节中的任何限制:“如果 T 是 cv std:: nullptr_t, 结果是一个空指针常量 ([conv.ptr])。 execution]),并且glvalue 可以引用联合的非活动成员 ([class.union])。— 尾注 ]"(强调)。
【解决方案2】:

这是未定义的行为,在标准中明确如此。您不能在一个中编写联合并在另一个中读取(期望适用)并在 C++ 中获得定义的行为。

但是,它也适用于您可能遇到的任何编译器,如果您使用 RGB 值和像素进行处理,您的代码很可能与平台相关联,因此我的建议是不要担心。

【讨论】:

  • 我宁愿static_assert(&pixel.array[0] == &pixel.r) 和其他两种颜色一样,所以它至少无法在失败的平台上编译。
  • @UlrichEckhardt 编译器在编译时对 UB 更加严格。即使这在实践中可行,我也希望它会失败。
  • @UlrichEckhardt 我只是tried it; GCC 不允许使用 static_assert,即使它明确允许通过联合进行类型双关作为语言扩展。
  • 查看godbolt.org/z/6Oiuds。您从原始问题中复制了一个错误(没有PixelRGB::rgb 的实例嵌套在联合中)。此外,您需要比较地址,而不是值。
  • @UlrichEckhardt 问题不仅仅是地址重叠之一。问题是允许编译器执行的优化之一。在我看来,编译器可以假设一个值永远不会通过不同的成员改变,因此它可以将一个值保存在寄存器中,并且如果它是通过不同的成员写入的,则无法从内存中重新读取其更新的值(例如) .
【解决方案3】:

显然,该假设可能取决于编译器。因此,我发现这是一种替代解决方案:

class PixelRGB
{

    public:

    unsigned char array[3];
    unsigned char &r;
    unsigned char &g;
    unsigned char &b;

    PixelRGB()
        : r{array[2]}
        , g{array[1]}
        , b{array[0]}
    {
    }
}

您可能需要注意您的字节顺序。如此明显的解决方案。它本质上是一个软件联盟。缺点是这个结构使用了更多的内存(除非编译器可以以某种方式优化引用?)——但这不是问题。将数据存储为unsigned char[],然后仅将其用于操作,这是我的预期目的。

【讨论】:

  • 无需担心字节顺序!字节序不会影响无符号字符。
  • @purpletentacle 读取数据时,您必须担心 rgb 像素顺序
  • @purpletentacle 我所说的只是r 可能是[0] 而不是[2]
  • 是的,RGB 顺序很重要!但这不是字节序
  • @user3728501:数组的顺序和三个 RGB 值的顺序都不是实现定义的。如果有一个 unsigned int 别名,它会出现在其中,但在这种情况下不会。
猜你喜欢
  • 2011-08-25
  • 1970-01-01
  • 2015-11-16
  • 1970-01-01
  • 2021-06-04
  • 2017-03-28
  • 1970-01-01
  • 2017-02-26
  • 1970-01-01
相关资源
最近更新 更多