【发布时间】:2020-04-27 07:26:06
【问题描述】:
假设我有一个 API:
// api.h - Others can #include this header
#include <cstdint>
class A {
public:
// Write data into an opaque type.
// The user shouldn't directly access the underlying bits of this value.
// They should use this method instead.
void WriteVal(uint32_t data);
private:
uint64_t opaque_value_;
};
// api.cpp
#include <cstdlib>
#include "api.h"
namespace {
// This is the underlying struct that the opaque value represents.
struct alignas(8) Impl {
uint32_t x;
uint32_t y;
};
} // namespace
void A::WriteVal(uint32_t data) {
uint64_t *opaque_ptr = &opaque_value_;
Impl *ptr = reinterpret_cast<Impl *>(opaque_ptr);
memcpy(&ptr->y, &data, sizeof(data));
}
A::WriteVal 方法中是否有任何未定义的行为?
出于以下原因,我的猜测是否定的:
- 重新解释
uint64_t *和Impl *之间的转换是可以的,因为指针类型的对齐方式是相同的。 - 如果要显式取消引用
ptr,则只有 UB,因为这会违反严格的别名规则。 -
无论指针的原始类型如何,
memcpy都可以安全地用于替代显式取消引用。
我的推理正确吗?如果这也被认为是 UB,那么是否有一种很好的 C++ 方式来写入不透明类型而没有非法类型双关语方法。
我的目标是干净地对不透明值执行操作,在底层,它代表用户不应该知道详细信息的结构。
【问题讨论】:
-
为什么需要
Impl?为什么不简单地将memcpy改为reinterpret_cast<char*>(&opaque_value_) + sizeof(uint32_t)?顺便说一句,您在&ptr中明确取消引用ptr,不是吗? -
您仍然有可能的结构填充问题。不过,正如 DanielLangr 所建议的那样,不需要该结构。
-
您可能还需要警惕字节序。
-
@DanielLangr 这是一个简化的例子。
Impl可以是具有任意数量的任意字段的结构来表示一些复杂的逻辑。这背后的想法是它更简洁,并更多地利用类型系统让编译器通过成员访问运算符获取成员的偏移量,而不是强制转换为char *并采用offsetof。此外,&ptr->y实际上并没有取消引用指针。这相当于你提到的演员然后偏移,但更清洁。如果您也为此查看程序集,您会发现ptr实际上从未被取消引用。 -
@LeoCHan 对不起,我忽略了自己,你是对的,你没有取消引用
ptr。但是,正如 Sander 指出的那样,填充有问题(虽然不太可能,但x和y之间可能存在一些问题)。另外,我不确定uint64_t的对齐是否保证为8。理论上,可能会更多。但是您可以静态断言Impl和uint64_t的大小和对齐方式相同。
标签: c++ undefined-behavior type-punning opaque-pointers