关于使用memcpy实现类型双关及在std::vector<char>中安全存储多类型变量的技术咨询
嘿,这个问题问得特别到位——类型双关和字节级存储的未定义行为(UB)确实是C++里很容易踩的坑,我来给你一步步捋清楚:
首先直接给你吃个定心丸:你用memcpy实现的Read/Write模板函数,在满足两个前提的情况下,是完全符合C++标准、没有UB的:
- 目标内存区域(也就是你传入的arr指向的vector
片段)有足够空间容纳T的所有字节(你已经明确提到了这一点,没问题) - 类型T是**可平凡复制(TriviallyCopyable)**的
这里得重点说下第二个前提:什么是可平凡复制类型?简单说就是那些可以通过纯字节复制完成完整复制的类型——比如int、double这些基本类型,以及没有自定义拷贝构造/赋值运算符、没有虚函数、所有成员都是可平凡复制的结构体/类。如果T是非平凡复制类型(比如带自定义拷贝逻辑的类、有虚函数的类),用memcpy复制字节会跳过对象的构造/拷贝逻辑,直接复制原始字节,这会导致对象处于无效状态,属于UB。不过你提到的int、double这类基础类型都是妥妥的可平凡复制类型,用这个方案完全没问题。
那之前的指针强制转换写法为什么是UB呢?说白了是违反了C的严格别名规则:C标准规定,你不能用一个类型的指针去访问另一个不相关类型的对象(除了用char访问任何类型的对象是允许的,但反过来不行)。你之前把char数组的地址转成int再解引用,相当于用int*去访问char类型的数组元素,这直接撞在了严格别名规则的枪口上,编译器完全可以对你的代码做一些你意想不到的优化,导致运行结果异常。而memcpy是纯字节级的复制,不涉及任何指针类型的别名访问,所以完全绕开了严格别名的限制,是合规的做法。
那如果你的场景需要支持非平凡复制类型怎么办?这时候memcpy就顶不住了,得换玩法:你需要在vector
// 假设要存一个非平凡类型MyClass,先确保vector有足够空间 vec.resize(vec.size() + sizeof(MyClass)); // 在vector的指定位置构造MyClass对象 MyClass* obj = new (&vec[position]) MyClass(/* 这里传构造参数 */); // 正常访问对象 obj->do_something(); // 用完必须显式调用析构函数销毁对象 obj->~MyClass();
不过这种方式需要你自己跟踪每个对象的类型、位置和生命周期,比memcpy的方案要繁琐不少。
当然,如果你不想自己折腾内存管理和类型跟踪,C++标准库其实已经给你准备好了更省心的方案:
- std::variant(C++17及以上):专门用来存储多种预定义类型的对象,类型安全,自动处理对象的构造和析构,比如你可以用
std::vector<std::variant<int, double, YourCustomType>>来直接存各种类型的对象,访问的时候用std::get或者std::visit就行,完全不用自己管字节的事,特别适合玩具解释器这种场景。 - std::any(C++17及以上):比variant更灵活,可以存储任何类型的对象,用类型擦除实现,访问的时候需要用
std::any_cast转换回原类型。缺点是性能比variant稍差一点,不过对于玩具解释器来说完全够用。
最后再给你划个重点:
- 你的memcpy方案对可平凡复制类型是100%安全的,比之前的指针强转靠谱多了
- 要支持非平凡类型就用placement new管理构造析构
- 想省心就直接上std::variant或std::any,标准库的轮子比自己造的稳多了
内容来源于stack exchange

