1

For instance, I have a structure definition

struct Data { 
    uint8_t data1; 
    uint16_t data2;
    virtual uint8_t getData1() { return data1; }
    virtual uint16_t getData2() { return data2; } 
}

I have a byte array

uint8_t data[3];

Is it safe to do this:

Data *d = (Data*)data;

I am asking because, i read that a class with virtual functions store a virtual table pointer and there's no standard defining it where it is stored in the object. Also, If I inherit from Data, for instance

struct Data2 : Data { uint8_t data3; 
virtual uint8_t getData3() { return data3; } }

What can be the order in which member variables in the object of Data2 are stored? If I cast Data2 structure over a byte array would it be in the order data1, data2, data3? Thank you in advance.

François Andrieux
  • 28,148
  • 6
  • 56
  • 87
PVRT
  • 480
  • 4
  • 13
  • 1
    That's not safe regardless of `virtual` because you're running into alignment issues (and `struct Data { uint8_t data1; uint16_t data2; }` will almost certainly have size 4, not 3). – melpomene Apr 13 '17 at 19:16
  • A preferred method is to assign the members individually from the buffer into the struct. This allows you to handle alignment and endian issues. – Thomas Matthews Apr 13 '17 at 19:28
  • Sorry, forgot to mention about storage allignment. Even with allignment macros as #pragma (pack, 1), would be safe to use it with a virtual function inside the structure? – PVRT Apr 13 '17 at 20:53

2 Answers2

4

In c++, a c style cast is interpreted as a an equivalent c++ cast, the most restrictive that can still accomplish the cast. In this case, it's reinterpret_cast.

The documentation on reinterpret_cast enumerates every defined use case. Unfortunately, your case is forbidden to dereference the resulting pointer. The presence of virtual methods has no bearing on this.

Note that it's legal to do the opposite and cast a Data * to a uint8_t * for the purpose of inspecting it's representation. It's also legal to cast such a uint8_t* back to Data*.

Edit: If your objective is to provide storage for an instance of Data, you may use std::aligned_storage and placement new. std::aligned_storage provides a safe memory location where an instance of a type may be constructed, and placement new allows you to specify where to construct an instance. However, this will not work well if you intend to store derived types.

François Andrieux
  • 28,148
  • 6
  • 56
  • 87
  • I am actually confused since you said forbidden and also legal to cast. – PVRT Apr 14 '17 at 06:23
  • We have a stream of bytes that can be from the network or just from a file. Currently, we do such a cast and obtain the data. My objective is to simplify the current process and make to easy to extend and maintain, ie. if a new version (structure with more members for example) comes to existance or preserving backward compatability. – PVRT Apr 14 '17 at 06:28
  • We allign the whole structure using pragma pack. – PVRT Apr 14 '17 at 06:31
  • @linux_freak It's legal to preform the cast, but it's forbidden to dereference the resulting pointer. One of the few things you *can* do with the result is to cast it back to `uint8_t*`. While your solution may work for you on your supported platform(s), it's not guaranteed to work by the standard, specially if your system involves network communications. Alignment, padding, endianness and, in some cases, member order can vary from platform to platform. The use of #pragma, by definition, involves implementation specific behavior. – François Andrieux Apr 14 '17 at 13:43
  • @linux_freak Because your type contains virtual methods, it does not qualify as a [POD type](http://en.cppreference.com/w/cpp/concept/PODType). You are required to construct ever instance using a constructor and you may not copy an instance by simply copying it's representation (it's bytes). Doing so is undefined behavior, – François Andrieux Apr 14 '17 at 13:45
1

Typically when a cast of array of bytes to some data structure occurs it's developer's responsibility to ensure the binary layout of this structure. In your example binary layout may vary quite a lot and presence of vtable stuff is only one of the problem. Another problem is alignment of fields. It typically depends on compilation options. For example if alignment is 4 bytes then the side of the structure will be at least 8 bytes + vtable-related pointers which clearly does not fit into array of 3 bytes. So performing cast and / or deep copy in this case will result in serious trouble.

To make sure that struct size is correct you can use #pragma pack or similar constructs and static assertions, like this:

#pragma pack(push, 1) // make sure that fields are packed

struct Data { 
uint8_t data1; 
uint16_t data2;
};

#pragma pack(pop) // restore initial alignment settings

static_assert(sizeof(Data) == 3, "Data struct layout is not correct");

Another problem is strict aliasing rules, which detonate undefined behavior because pointer to Data is not allowed to alias a pointer to uint8_t. So in this case a double cast (or deep copy) is required so compiler won't make too many assumptions about pointers:

Data *d = reinterpret_cast< Data * >(reinterpret_cast< ::std::uintptr_t >(data));

And yet another issue could be different endianness of uint16_t field written in array, but unfortunately there is no straight way to deal with it.

Community
  • 1
  • 1
user7860670
  • 35,849
  • 4
  • 58
  • 84