类型双关主题的变化:就地琐碎构造


9

我知道这是一个很普通的主题,但是尽管很容易找到典型的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链接,如果您想使用它。

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


2
但是连续Pixel放置的Pixels 仍然不是s 的数组。
Jarod42

1
@spectras那虽然不能构成数组。您只有一堆彼此相邻的Pixel对象。这与数组不同。
NathanOliver

1
因此,没有,你在哪里做的pixels[some_index]还是*(pixels + something)?那将是UB。
NathanOliver

1
相关部分在这里,关键短语是P是否指向数组对象x的数组元素i。这里pixels(P)不是指向数组对象的指针,而是指向single的指针Pixel。这意味着您只能pixels[0]合法访问。
NathanOliver

3
您想阅读wg21.link/P0593
ecatmur

Answers:


3

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

否则,如果if P指向ix具有n元素([dcl.array])的数组对象的数组元素,则表达式P + Jand J + P(where J的值是j)指向if 的(可能是假设的)数组元素 i+j,而表达式则指向(可能-假设的)数组元素 的如果。x0≤i+j≤nP - Ji−jx0≤i−j≤n

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


感谢您快速发现。我猜我会做一个迭代器。作为旁注,这也意味着&somevector[0] + 1是UB(嗯,我的意思是,使用结果指针将是UB)。
光谱

@spectras没关系。您始终可以将指针指向一个对象。即使那里没有有效的对象,您也无法取消引用该指针。
NathanOliver

是的,我编辑了注释以使自己更清楚,我的意思是取消对结果指针的引用:)谢谢您的确认。
光谱

@spectras没问题。C ++的这一部分可能非常困难。即使硬件可以完成我们想要的工作,但这实际上并不是编码。我们正在使用C ++抽象机进行编码,并且它是一台灵活的机器;)希望P0593将被采用,并且这将变得更加容易。
NathanOliver

1
@spectras否,因为标准向量定义为包含数组,因此您可以在数组元素之间进行指针算术运算。不幸的是,没有碰到UB,就无法在C ++本身中实现std向量。
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对象在技术上将是未定义的行为。

我认为,即使您reinterpret_cast在创建Pixel对象之后进行此操作,这仍然成立,因为该Pixel对象及其std::byte为其提供存储的对象不是指针可互换的。因此,即使那样,指针也将继续指向std::byte,而不是Pixel对象。

如果从新放置之一的结果中获得了要返回的指针,那么就可以访问该特定Pixel对象而言,一切都应该没问题。


另外,您还需要确保std::byte指针正确对齐,Pixel并且数组确实足够大。据我所知,该标准实际上并没有要求Pixel具有相同的对齐方式std::byte或没有填充。


同样,这都不取决于Pixel琐事或其他任何其他属性。只要std::byte数组具有足够的大小并针对Pixel对象适当对齐,所有内容的行为方式都相同。


我相信那是正确的。即使阵列东西(的unimplementability std::vector)不是一个问题,你还是会需要std::launder的结果访问任何的展示位置定位的前new主编Pixel秒。到目前为止,std::launder这里是UB,因为相邻的Pixels可以从经过清洗的指针访问。
Fureeish

@Fureeish我不确定std::launder如果result返回之前应用到UB 为什么会是UB 。根据我对eel.is/c++draft/ptr.launder#4的理解,相邻的Pixel对象不能通过洗过的指针“ 到达 ” 。甚至是我也不知道它是如何UB,因为整个原始数组都可以从原始指针访问std::byte
胡桃

但是下一个Pixel将无法从std::byte指针访问,而是从laundered指针访问。我相信与这里有关。不过,我很高兴得到纠正。
Fureeish

@Fureeish据我所知,此处给出的所有示例均不适用,要求的定义也与标准相同。可达性是根据存储字节(而不是对象)定义的。Pixel从我的原始指针看,下一个占用的字节对我来说似乎是可以到达的,因为原始指针指向std::byte数组的一个元素,该元素包含构成Pixel“” 或Z是元素 “条件适用(位置ZY,即std::byte元素本身)。
胡桃

我认为,下一个Pixel占用的存储字节是无法通过清洗后的指针访问的,因为指向的Pixel对象不是数组对象的元素,并且也不能与任何其他相关对象进行指针互换。但是我也std::launder第一次在这个深度上考虑这个细节。我也不是对此100%肯定。
胡桃
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.