类型双关主题的变体:就地琐碎构造
Variation on the type punning theme: in-place trivial construction
我知道这是一个很常见的主题,但是尽管典型的 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
)
将promote
的结果作为数组使用是未定义的行为。如果我们查看 [expr.add]/4.2 我们有
Otherwise, if P
points to an array element i
of an array object x
with n
elements ([dcl.array]), the expressions P + J
and J + P
(where J
has the value j
) point to the (possibly-hypothetical) array element
i+j
of x
if 0≤i+j≤n
and the expression P - J
points to the (possibly-hypothetical) array element
i−j
of x
if 0≤i−j≤n
.
我们看到它需要指针实际指向一个数组对象。你实际上并没有数组对象。你有一个指向单个 Pixel
的指针,它恰好在连续内存中有其他 Pixels
跟随它。这意味着您实际可以访问的唯一元素是第一个元素。尝试访问其他任何内容都将是未定义的行为,因为您已经超出了指针有效域的末尾。
关于 returned 指针的有限使用,您已经有了答案,但我想补充一点,我还认为您需要 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
因为它不是 pointer-interconvertible。所以即便如此,指针仍将指向 std::byte
,而不是 Pixel
对象。
如果您从其中一个 placement-new 的结果中获得指向 return 的指针,那么就访问特定 Pixel
对象而言,一切都应该没问题。
您还需要确保 std::byte
指针与 Pixel
适当对齐,并且数组确实足够大。据我所知,该标准并不真正要求 Pixel
与 std::byte
具有相同的对齐方式或者它没有填充。
此外 none 这取决于 Pixel
是微不足道的还是它的任何其他 属性。只要 std::byte
数组有足够的大小并且适合 Pixel
对象对齐,一切都会以相同的方式运行。
我知道这是一个很常见的主题,但是尽管典型的 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
)
将promote
的结果作为数组使用是未定义的行为。如果我们查看 [expr.add]/4.2 我们有
Otherwise, if
P
points to an array elementi
of an array objectx
withn
elements ([dcl.array]), the expressionsP + J
andJ + P
(whereJ
has the valuej
) point to the (possibly-hypothetical) array elementi+j
ofx
if0≤i+j≤n
and the expressionP - J
points to the (possibly-hypothetical) array elementi−j
ofx
if0≤i−j≤n
.
我们看到它需要指针实际指向一个数组对象。你实际上并没有数组对象。你有一个指向单个 Pixel
的指针,它恰好在连续内存中有其他 Pixels
跟随它。这意味着您实际可以访问的唯一元素是第一个元素。尝试访问其他任何内容都将是未定义的行为,因为您已经超出了指针有效域的末尾。
关于 returned 指针的有限使用,您已经有了答案,但我想补充一点,我还认为您需要 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
因为它不是 pointer-interconvertible。所以即便如此,指针仍将指向 std::byte
,而不是 Pixel
对象。
如果您从其中一个 placement-new 的结果中获得指向 return 的指针,那么就访问特定 Pixel
对象而言,一切都应该没问题。
您还需要确保 std::byte
指针与 Pixel
适当对齐,并且数组确实足够大。据我所知,该标准并不真正要求 Pixel
与 std::byte
具有相同的对齐方式或者它没有填充。
此外 none 这取决于 Pixel
是微不足道的还是它的任何其他 属性。只要 std::byte
数组有足够的大小并且适合 Pixel
对象对齐,一切都会以相同的方式运行。