我编写了一个库,其接口引用来自客户端的坐标数据。客户需要提供一系列点,每个点包含 3 个连续的
float
。我决定使用跨步数组表示,以便客户端可以继续使用自己的数据类型。
假设客户端在内部使用以下
Point
结构(它对库不可见):
struct Point {
float[3] xyz;
CustomData data;
// more data
};
客户端应将一些
Point
存储在连续的存储中,并将相关信息传递到我的库:
struct Interface {
const float* first_x; // Pointer to xyz[0] in first Point
std::ptrdiff_t stride; // Set to sizeof(Point)
std::ptrdiff_t len; // Number of contiguous Point
};
我可以用标准 C++ 安全地访问每个点的坐标吗?在我看来,某种形式的字节指针运算是不可避免的。
现在,我像这样读取坐标:
void Interface::process_points()
{
for (std::ptrdiff_t i = 0; i < len; ++i) {
const float* const pf = reinterpret_cast<const float*>(reinterpret_cast<const char*>(first_x) + i * stride);
float x = pf[0];
float y = pf[1];
float z = pf[2];
// do things with x, y, z
}
}
通过增加别名
char
指针来跳过步幅。目前它似乎工作正常,但是我不知道指针算术和 char
别名的这种混合是否是 C++ 中定义良好的行为。
只有当您的库可以在编译时获取
struct Point
的大小时,才可以以明确定义的方式执行此操作。可以按如下方式完成:
#include <bit>
static constexpr auto POINT_SIZE = ...;
using point_sized_float_array_t = std::array<float, POINT_SIZE / sizeof(float)>;
using point_sized_byte_array_t = std::array<std::byte, POINT_SIZE / sizeof(std::byte)>;
void process_points(const float* points, std::size_t point_count)
{
const auto points_bytes_ptr = reinterpret_cast<const std::byte*>(points);
for (std::ptrdiff_t i = 0; i < point_count; ++i)
{
const auto point_bytes_ptr = points_bytes_ptr + i * POINT_SIZE;
point_sized_byte_array_t point_bytes{};
std::copy_n(points_bytes_ptr, point_bytes.size(), point_bytes.begin());
const auto point_as_floats = std::bit_cast<point_sized_float_array_t>(point_bytes);
const float x = point_as_floats[0];
const float y = point_as_floats[1];
const float z = point_as_floats[2];
}
}
要点
唯一使用的
reinterpret_cast
是reinterpret_cast<const std::byte*>()
,以后可以安全地访问。这是类字节类型(char
、unsigned char
、std::byte
)所特有的,因为从 reinterpret_cast
的结果读取值到(几乎)任何其他类型都会导致未定义的行为。
std::bit_cast
用于将字节数组转换为浮点数组。只要源类型和目标类型具有相同的大小并且可以轻松复制,这就是安全且定义良好的。可以安全地读取转换的结果,因为它创建值的副本,而不是引用,因此避免与严格的别名规则发生冲突。
因此,可以读取
x
、y
和z
中保留的值,并且程序不会导致未定义的行为。然而,重要的是要记住,这些值的正确性实际上可能并不能得到保证。这就是说,如果您正在运行一些奇异的编译器或操作系统,您可能会得到垃圾值。