【问题标题】:Variation on the type punning theme: in-place trivial construction类型双关语主题的变化:就地琐碎构造
【发布时间】:2020-05-01 09:59:17
【问题描述】:

我知道这是一个很常见的主题,但尽管很容易找到典型的 UB,但到目前为止我还没有找到这个变体。

所以,我尝试正式引入 Pixel 对象,同时避免数据的实际副本。

这有效吗?

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 link 如果你想玩它。

(注意:尽管有std::byte,但我没有标记c++17,因为问题使用char

【问题讨论】:

  • 但是连续的Pixels 放置新的仍然不是Pixels 的数组。
  • @spectras 但这并没有创建一个数组。你只有一堆相邻的像素对象。这与数组不同。
  • 那么你在哪里做pixels[some_index]*(pixels + something)?那将是UB。
  • 相关部分是here,关键短语是如果P指向数组对象x的数组元素i。这里pixels (P) 不是指向数组对象的指针,而是指向单个Pixel 的指针。这意味着您只能合法访问pixels[0]
  • 你想看wg21.link/P0593

标签: c++ language-lawyer reinterpret-cast object-lifetime placement-new


【解决方案1】:

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

否则,如果 P 指向数组对象 x 的数组元素 in 元素 ([dcl.array]),则表达式 P + JJ + P(其中J 的值为j)指向(可能是假设的)数组元素 i+j of x if 0≤i+j≤n 和表达式 P - J 指向(可能是假设的)数组元素 i−j of x if 0≤i−j≤n.

我们看到它需要指针实际指向一个数组对象。不过,您实际上并没有数组对象。您有一个指向单个 Pixel 的指针,而该指针恰好在连续内存中紧随其后有其他 Pixels。这意味着您可以实际访问的唯一元素是第一个元素。尝试访问其他任何内容都是未定义的行为,因为您超出了指针的有效域的末尾。

【讨论】:

  • 感谢您这么快发现。我猜我会做一个迭代器。作为旁注,这也意味着&amp;somevector[0] + 1 是UB(嗯,我的意思是,使用结果指针会是)。
  • @spectras 其实没关系。您始终可以获取指向对象的指针。即使那里有一个有效的对象,您也不能取消引用该指针。
  • 是的,我编辑了评论以使自己更清楚,我的意思是取消引用结果指针:) 感谢您的确认。
  • @spectras 没问题。 C++ 的这一部分可能非常困难。即使硬件会做我们想让它做的事情,但这实际上并不是编码的目的。我们正在编写 C++ 抽象机器,它是一个挑剔的机器;)希望 P0593 能够被采用,这会变得容易得多。
  • @spectras 不,因为标准向量被定义为包含一个数组,并且您可以在数组元素之间进行指针运算。遗憾的是,没有办法在 C++ 本身中实现标准向量,而不会遇到 UB。
【解决方案2】:

您已经对返回的指针的有限使用有了答案,但我想补充一点,我还认为您需要 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 对象。

如果您从一个placement-new的结果中获得了要返回的指针,那么一切都应该没问题,就访问特定的Pixel对象而言。


您还需要确保std::byte 指针与Pixel 适当对齐,并且数组确实足够大。据我所知,该标准并不真正要求 Pixelstd::byte 具有相同的对齐方式,或者它没有填充。


此外,这一切都不取决于Pixel 是微不足道的,或者实际上是它的任何其他属性。只要std::byte 数组的大小足够大并且与Pixel 对象对齐,一切都会以相同的方式运行。

【讨论】:

  • 我相信这是正确的。即使数组事物(std::vector 的不可实现性)不是问题,您仍然需要在访问任何位置之前std::launder 结果-newed Pixels。截至目前,std::launder 这里是 UB,因为可以从 laundered 指针访问相邻的 Pixels。
  • @Fureeish 我不确定如果在返回之前将std::launder 应用于result,为什么会成为UB。根据我对eel.is/c++draft/ptr.launder#4 的理解,通过清洗过的指针,相邻的Pixel 不是“可达”。甚至它是我看不到它是如何 UB,因为整个原始 std::byte 数组从原始指针 可达
  • 但是下一个Pixel 将无法从std::byte 指针访问,但它来自laundered 指针。我相信this 在这里是相关的。不过,我很高兴得到纠正。
  • @Fureeish 据我所知,这里给出的示例均不适用,并且要求的定义也与标准相同。可达性是根据存储字节定义的,而不是对象。下一个Pixel 占用的字节对我来说似乎可以从原始指针到达,因为原始指针指向std::byte 数组的一个元素,该数组包含构成Pixel 存储的字节,使得“或在 Z 是元素的直接封闭数组中”条件适用(其中ZY,即std::byte 元素本身)。
  • 我认为下一个Pixel 占用的存储字节无法通过清洗过的指针访问,因为指向的Pixel 对象不是数组对象的元素,也是不能与任何其他相关对象进行指针互转换。但我也第一次深入思考std::launder 的这个细节。我也不是 100% 确定这一点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-19
  • 1970-01-01
  • 1970-01-01
  • 2011-08-08
  • 2014-11-16
相关资源
最近更新 更多