我需要处理作为字符缓冲区提供给我的数据,其中数据的实际结构取决于其某些字段的值。
更具体地说,考虑以下头文件:
struct IncomingMsgStruct
{
MsgHdrStruct msgHdr;
char msgData[MSG_DATA_MAX_SIZE]; // Can hold any of several structures
};
struct RelevantMessageData
{
DateTimeStruct dateTime;
CommonDataStruct commonData;
MsgBodyUnion msgBody;
};
struct DateTimeStruct { /* ... */ };
struct CommonDataStruct
{
char name[NAME_MAX_SIZE + 1];
MsgTypeEnum msgType;
// more elements here
};
union MsgBodyUnion
{
MsgBodyType1Struct msgBodyType1;
MsgBodyType2Struct msgBodyType2;
// ...
MsgBodyTypeNStruct msgBodyTypeN;
};
struct MsgBodyType1Struct { /* ... */ };
struct MsgBodyType2Struct { /* ... */ };
// ...
struct MsgBodyTypeNStruct { /* ... */ };
结构包含数据成员(其中一些也是结构(和用于初始化、转换为字符串等的成员函数。没有构造函数、析构函数、虚拟函数或继承。
请注意,这是在我无法控制的遗留代码的上下文中。标头和其中的定义由其他组件使用,其中一些组件可能会随着时间的推移而更改。
数据是作为字符缓冲区提供给我的,所以我的处理功能看起来像:
ResultType processRelevantMessage(char const* inBuffer);
保证inBuffer
包含MsgStruct
结构,并且其msgData
成员保持RelevantMessageData
结构。由于数据来源于同一平台上的相应结构,因此也保证了正确的对齐和端序。
为了简单起见,假设我只对msgType
等于特定值的情况感兴趣,所以只需要访问的成员,比如MsgBodyType2Struct
(否则会返回错误(。稍后我可以将其概括为处理几种类型。
我的理解是,使用reinterpret_cast
的天真实现可能会违反C++严格的别名规则。
我的问题是:
如何在符合标准的C++中做到这一点,而不调用未定义的行为,不更改或复制定义,也不进行额外的复制或分配?
或者,如果这不可能,我如何在GCC中做到这一点(可能使用-fno-strict-aliasing
等标志(?
编辑:
由于数据来自同一平台,因此不应存在端序问题。
如上所述,我更喜欢避免复制。
经过进一步的阅读,在我看来,新的安置应该是安全的。那么,以下实施是否符合要求?
ResultType processRelevantMessageType2(char const* in)
{
MsgStruct const* pMsgStruct = new (in) MsgStruct;
RelevantMessageData const* pRelevantMessageData = new (pMsgStruct->msgData) RelevantMessageData;
// Assume we're only interested in the MsgBodyType2Struct case
if (pRelevantMessageData->commonData.msgType == MSG_TYPE_2) {
MsgBodyType2Struct const& msgBodyType2Struct = pRelevantMessageData->msgBody.MsgBodyType2Struct;
// Can access the fields of msgBodyType2Struct here?
// ...
}
// ...
}
我的理解是,使用
reinterpret_cast
的天真实现可能会违反C++严格的别名规则。
确实如此。此外,考虑字节数组可能从内存中的任意地址开始,而struct
通常有一些需要满足的对齐限制。处理此问题最安全的方法是创建一个所需类型的新对象,并使用std::memcpy()
将缓冲区中的字节复制到对象中:
ResultType processRelevantMessage(char const* inBuffer) {
MsgHdrStruct hdr;
std::memcpy(&hdr, inbuffer, sizeof hdr);
...
RelevantStruct data;
std::memcpy(&data, inbuffer + sizeof hdr, sizeof data);
...
}
以上是定义良好的C++代码,之后可以毫无问题地使用hdr
和data
(只要它们是不包含任何指针的POD类型(。
我建议使用序列化库或为这些struct
编写operator<<
和operator>>
重载。您可以使用某些平台上提供的函数htonl
和ntohl
,或者编写一个支持类来自己流式传输数值。
这样一个类可能看起来像这样:
#include <bit>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <iterator>
#include <limits>
#include <type_traits>
template<class T>
struct tfnet { // to/from net (or file)
static_assert(std::endian::native == std::endian::little ||
std::endian::native == std::endian::big); // endianess must be known
static_assert(std::numeric_limits<double>::is_iec559); // only support IEEE754
static_assert(std::is_arithmetic_v<T>); // only for arithmetic types
tfnet(T& v) : val(&v) {} // store a pointer to the value to be streamed
// write a value to a stream
friend std::ostream& operator<<(std::ostream& os, const tfnet& n) {
if constexpr(std::endian::native == std::endian::little) {
// reverse byte order to be in network byte order
char buf[sizeof(T)];
std::memcpy(buf, n.val, sizeof buf);
std::reverse(std::begin(buf), std::end(buf));
os.write(buf, sizeof buf);
} else {
// already in network byte order
os.write(n.val, sizeof(T));
}
return os;
}
// read a value from a stream
friend std::istream& operator>>(std::istream& is, const tfnet& n) {
char buf[sizeof(T)];
if(is.read(buf, sizeof buf)) {
if constexpr(std::endian::native == std::endian::little) {
// reverse byte order to be in network byte order
std::reverse(std::begin(buf), std::end(buf));
}
std::memcpy(n.val, buf, sizeof buf);
}
return is;
}
T* val;
};
现在,如果你有一套struct
s:
#include <cstdint>
struct data {
std::uint16_t x = 10;
std::uint32_t y = 20;
std::uint64_t z = 30;
};
struct compound {
data x;
int y = 40;
};
你可以为他们添加流媒体运营商:
std::ostream& operator<<(std::ostream& os, const data& d) {
return os << tfnet{d.x} << tfnet{d.y} << tfnet{d.z};
}
std::istream& operator>>(std::istream& is, data& d) {
return is >> tfnet{d.x} >> tfnet{d.y} >> tfnet{d.z};
}
std::ostream& operator<<(std::ostream& os, const compound& d) {
return os << d.x << tfnet{d.y}; // using data's operator<< for d.x
}
std::istream& operator>>(std::istream& is, compound& d) {
return is >> d.x >> tfnet{d.y}; // using data's operator>> for d.x
}
以及读/写struct
s:
#include <sstream>
int main() {
std::stringstream ss;
compound x;
compound y{{0,0,0},0};
ss << x; // write to stream
ss >> y; // read from stream
}
演示
如果不能直接在源流上使用流运算符,可以将获得的char
缓冲区放在istringstream
中,并使用添加的运算符从中提取数据。