类型游戏的变种:原地平凡构造

9
我知道这是一个相当普遍的主题,但尽管通常的UB很容易找到,但我到目前为止没有找到这个变体。
所以,我正在尝试在避免实际复制数据的情况下正式引入像素对象。
这是有效的吗?
struct Pixel {
    uint8_t red;
    uint8_t green;
    uint8_t blue;
    uint8_t alpha;
};

static_assert(std::is_trivial_v<Pixel>);

Pixel* promote(std::byte* data, std::size_t count)
{
    Pixel * const result = reinterpret_cast<Pixel*>(data);
    while (count-- > 0) {
        new (data) Pixel{
            std::to_integer<uint8_t>(data[0]),
            std::to_integer<uint8_t>(data[1]),
            std::to_integer<uint8_t>(data[2]),
            std::to_integer<uint8_t>(data[3])
        };
        data += sizeof(Pixel);
    }
    return result; // throw in a std::launder? I believe it is not mandatory here.
}

预期使用模式,大大简化:
std::byte * buffer = getSomeImageData();
auto pixels = promote(buffer, 800*600);
// manipulate pixel data

更具体地说:
  • 这段代码是否有明确定义的行为?
  • 如果是,使用返回的指针是否安全?
  • 如果是,可以将其扩展到哪些其他Pixel类型?(放宽is_trivial限制?仅具有3个组件的像素?)

无论是clang还是gcc都会将整个循环优化为空,这正是我想要的。现在,我想知道这是否违反了某些C ++规则。

Godbolt链接,如果您想尝试一下。

(注:尽管使用了 std::byte,但我没有标记为 c++17,因为问题可以使用 char 解决。)

2
但是,相邻的“Pixel”放置在一起仍然不是一个“Pixel”数组。 - Jarod42
1
@ spectras 那并不会形成一个数组。您只是将一堆像素对象放在一起。这与数组不同。 - NathanOliver
1
相关部分在这里,关键词是if P points to an array element i of an array object x。在这里,pixels(P)不是指向数组对象的指针,而是指向单个Pixel的指针。这意味着您只能合法地访问pixels [0] - NathanOliver
3
你想要阅读http://wg21.link/P0593。 - ecatmur
1
没有一天不会有与P0593相关的(重复)问题。 - Language Lawyer
显示剩余8条评论
2个回答

3
promote 的结果用作数组是未定义行为。如果我们查看 [expr.add]/4.2,我们可以看到:

否则,如果 P 指向数组对象 x 的数组元素 i(具有 n 个元素 ([dcl.array)]),表达式 P + JJ + P(其中 J 的值为 j)指向(可能是虚构的)数组元素 i+j,如果 0≤i+j≤n ,表达式 P - J 指向(可能是虚构的)数组元素 i−j,如果 0≤i−j≤n

我们可以看到它要求指针实际上指向一个数组对象。但是您实际上没有一个数组对象。您只有一个指向单个 Pixel 的指针,它恰好在相邻的内存中有其他 Pixel。这意味着您实际上只能访问第一个元素。尝试访问其他任何元素都将是未定义的行为,因为您超出了指针有效域的末尾。

谢谢你快速找出了这个问题。我想我会使用迭代器来解决它。顺便说一下,这也意味着&somevector[0] + 1是未定义的行为(我的意思是,使用结果指针是未定义的)。 - spectras
@spectras 这其实没关系。你总是可以获取到一个对象的指针。只是你不能对这个指针进行解引用,即使那里有一个有效的对象。 - NathanOliver
是的,我编辑了评论以使自己更清楚,我指的是取消引用结果指针 :) 谢谢确认。 - spectras
@ spectras 没问题。 C ++ 的这部分可能非常困难。 即使硬件可以按照我们的意愿执行,但这实际上并不是我们编码的内容。 我们正在编写 C ++ 抽象机器,并且它是一个挑剔的机器 ;) 希望 P0593 能够被采纳,这样就会变得更容易。 - NathanOliver
我知道,这就是为什么我总是通过人来双重检查它们,而不是依赖工具。这是遵守C++规则以保护免受后续意外事情的影响。P0593看起来确实很有趣,但我对于必须针对如此简单的东西拥有一个迭代器感到有点难过(尽管幸运的是,它也可以编译成无开销代码)。 - spectras
1
@spectras 不行,因为std vector被定义为包含一个数组,并且你可以在数组元素之间进行指针算术运算。很遗憾,在C++本身中没有办法实现std vector而不会遇到未定义行为。 - Yakk - Adam Nevraumont

1

关于返回指针的有限使用,你已经有了一个答案,但我想补充一点,即我认为你甚至需要使用 std::launder 才能访问第一个 Pixel

reinterpret_cast 是在创建任何 Pixel 对象之前完成的(假设你没有在 getSomeImageData 中执行此操作)。因此,reinterpret_cast 不会更改指针值。结果指针仍将指向传递给函数的 std::byte 数组的第一个元素。

当你创建 Pixel 对象时,它们将被嵌套在 std::byte 数组中,并且 std::byte 数组将为 Pixel 对象提供存储。

有些情况下,重复使用存储空间会导致指针自动指向新对象的旧对象。但这并不是正在发生的事情,因此result仍将指向std::byte对象,而不是Pixel对象。我想,把它用作指向Pixel对象的指针在技术上将成为未定义行为。
我认为,即使在创建Pixel对象之后进行reinterpret_cast,这个仍然成立,因为Pixel对象和提供其存储的std::byte不是指针可互换的。因此,即使那时指针仍将继续指向std::byte,而不是Pixel对象。
如果您从放置new的结果中获取要返回的指针,则就访问该特定Pixel对象而言,一切都应该没问题。
此外,您需要确保std::byte指针适合Pixel,并且数组确实足够大。就我所记得的而言,标准并不真正要求Pixel具有与std::byte相同的对齐方式或者没有填充。
此外,这一切都不取决于Pixel是否微不足道或其他任何属性。只要std::byte数组具有足够的大小并且适当对齐Pixel对象,所有内容都将以相同的方式运作。

我相信那是正确的。即使数组的问题(std::vector不可实现)不是问题,您仍需要在访问放置的 Pixel 之前使用 std::launder 对结果进行处理。就目前而言,在这里使用 std::launder 是未定义行为,因为清理干净的指针可以访问相邻的 Pixel - Fureeish
@Fureeish 我不确定为什么在返回 result 之前对其应用 std::launder 将被视为未定义行为。根据我对 https://eel.is/c++draft/ptr.launder#4 的理解,相邻的 Pixel 不能通过清理后的指针 "reachable"。即使它能够被访问,我也看不出这是未定义行为,因为整个原始的 std::byte 数组都可以从原始指针中 reachable - walnut
但是下一个 Pixel 将无法从 std::byte 指针访问,但可以从 laundered 指针访问。我认为这里与此相关。不过,如果有错误,请指正。 - Fureeish
据我所知,这里提供的示例都不适用,并且需求的定义与标准也相同。可达性是以存储字节为单位定义的,而不是对象。下一个“Pixel”占用的字节似乎从原始指针可以到达,因为原始指针指向“std::byte”数组的一个元素,该数组包含构成“Pixel”存储的字节,因此满足“或者在Z作为元素的紧密封闭数组内”的条件(其中“Z”为“Y”,即“std::byte”元素本身)。 - walnut
我认为下一个“Pixel”所占用的存储字节不可通过洗涤后的指针访问,因为所指向的“Pixel”对象既不是数组对象的元素,也不与任何其他相关对象进行指针相互转换。但我也是第一次深入考虑“std::launder”的这个细节。我也不确定100%。 - walnut

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接