Consider the following struct that contains some environment values:
struct environment_values {
uint16_t humidity;
uint16_t temperature;
uint16_t charging;
};
I would like to add some additional information to those values with a phantom type* and make their types distinct at the same time:
template <typename T, typename P>
struct Tagged {
T value;
};
// Actual implementation will contain some more features
struct Celsius{};
struct Power{};
struct Percent{};
struct Environment {
Tagged<uint16_t,Percent> humidity;
Tagged<uint16_t,Celsius> temperature;
Tagged<uint16_t,Power> charging;
};
Is the memory-layout of Environment
the same as environment_values
? Does this also hold for mixed type layouts, e.g.:
struct foo {
uint16_t value1;
uint8_t value2;
uint64_t value3;
}
struct Foo {
Tagged<uint16_t, Foo> Value1;
Tagged<uint8_t , Bar> Value2;
Tagged<uint64_t, Quux> Value3;
}
For all types I've tried so far, the following assertions held:
template <typename T, typename P = int>
constexpr void check() {
static_assert(alignof(T) == alignof(Tagged<T,P>), "alignment differs");
static_assert(sizeof(T) == sizeof(Tagged<T,P>), "size differs");
}
// check<uint16_t>(), check<uint32_t>(), check<char>() …
Since the size of the tagged and untagged variants is also the same, I guess the answer should be yes, but I would like to have some certainty.
* I have no idea how those tagged values are called in C++. "Strongly typed typedefs"? I've taken the name from Haskell.
The Standard mentions in [basic.align]/1:
Object types have alignment requirements (3.9.1, 3.9.2) which place restrictions on the addresses at which an object of that type may be allocated. An alignment is an implementation-defined integer value representing the number of bytes between successive addresses at which a given object can be allocated. An object type imposes an alignment requirement on every object of that type; stricter alignment can be requested using the alignment specifier (7.6.2).
Moreover, [basic.compound]/3, mentions:
The value representation of pointer types is implementation-defined. Pointers to layout-compatible types shall have the same value representation and alignment requirements (6.11). [Note: Pointers to over-aligned types (6.11) have no special representation, but their range of valid values is restricted by the extended alignment requirement].
As a result, there is a guarantee that layout-compatible types have the same alignment.
struct { T m; }
and T
are not layout-compatible.
As pointed here, in order for two elements to be layout compatible then they both have to be standard-layout types, and their non-static data members must occur with the same types and in the same order.
struct { T m; }
contains just a T
, but T
is a T
so it cannot contain a T
as its first non-static data member.
According to the letter of the law, size and alignment of types is implementation-defined and the standard gives you few if any guarantees about what sizeof
and alignof
will return.
template <typename T, typename P>
struct Tagged {
T value;
};
In theory, the compiler is permitted to add padding to the end of this struct, which would obviously alter the size and probably the alignment as well. In practise, the only time I could envisage this happening is if T
was given some sort of compiler-specific "packed" attribute, but Tagged
was not (but even then, GCC seems to work okay).
In any case, I'd say it would be a good idea to add some static asserts to ensure that the compiler is being sensible -- which is exactly what you've done :).
As mentioned by gsamaras, the standard guarantees the same alignment for layout compatible classes.
Unfortunately, struct { T m; }
and T
are not layout compatible.
In 12.2.21 the standard lays out the requirements for a layout compatible class:
Two standard-layout struct (Clause 12) types are layout-compatible classes if their common initial sequence comprises all members and bit-fields of both classes (6.9).
And the definition of common initial sequence is in 12.2.20:
The common initial sequence of two standard-layout struct (Clause 12) types is the longest sequence of non-static data members and bit-fields in declaration order, starting with the first such entity in each of the structs, such that corresponding entities have layout-compatible types and either neither entity is a bit-field or both are bit-fields with the same width. [Example:
struct A { int a; char b; };
struct B { const int b1; volatile char b2; };
struct C { int c; unsigned : 0; char b; };
struct D { int d; char b : 4; };
struct E { unsigned int e; char b; };
The common initial sequence ofA
andB
comprises all members of either class. The common initial sequence ofA
andC
and ofA
andD
comprises the first member in each case. The common initial sequence ofA
andE
is empty.
— end example]
So from this we can make the following important observations:
- Layout compatibility is limited strictly to standard layout classes. (Or enums use the same underlying type or the trivial case when
T
andT2
are literally the exact same type. See 6.9.11.) In the general case,T
is not a standard layout class. In fact,T
is not even a class in your example (it is auint16_t
, believe it or not, this matters according to the standard.)* - Even if
T
is guaranteed to be a standard layout class,struct { T m; }
does not have a common initial sequence withT
. The sequence ofstruct { T m; }
begins withT
, whereas the sequence ofT
begins with whateverT
's non-static data members are. This is actually strictly guaranteed not to be aT
as a class cannot contain itself by value.
Therefore, the guarantee cannot be held by the letter of the standard. You should continue to perform the static_assert
ions to ensure your compiler is behaving in the fashion you expect.
* See most questions on union type punning.
来源:https://stackoverflow.com/questions/46425250/does-a-phantom-type-have-the-same-alignment-as-the-original-one