My coworker wants to send some data represented by a type T over a network. He does this The traditional way™ by casting the T to char* and sending it using a write(2) call with a socket:
auto send_some_t(int sock, T const* p) -> void
{
auto buffer = reinterpret_cast<char const*>(p);
write(sock, buffer, sizeof(T));
}
So far, so good. This simplified example, apart from being stripped of any error-checking, should be correct. Assuming the type T is trivially copyable we can copy values of this type between objects using std::mempcy() (according to 6.7 [basic.types] point 3 in C++17 standard[1]) so I guess write(2) should also work as it blindly copies binary data.
Where it gets tricky is on the receiving side.
Assume the type T in question looks like this:
struct T {
uint64_t foo;
uint8_t bar;
uint16_t baz;
};
It has a field with an alignment requirement of 8 bytes (foo) so the whole type requires a strict alignment of 8 bytes (see example for 6.6.5 [basic.align] point 2). This means that storage for values of type T must be allocated only on suitable addresses.
Now, what about the following code?
auto receive_some_t(int sock, T* p) -> void
{
read(sock, p, sizeof(T));
}
// ...
T value;
receive_some_t(sock, &T);
Looks shady, but should work OK. The bytes received do represent a valid value of type T and are blindly copied into a valid object of type T.
However, what about using raw char buffers like in the following code:
char buffer[sizeof(T)];
read_some_t(sock, buffer);
T* value = reinterpret_cast<T*>(buffer);
This is where my coder-brain triggers a red alert. We have absolutely no guarantee that the alignment of char[sizeof(T)] matches that of T which is a problem. We also do not round-trip a pointer to a valid T object because there wasn't a valid object of type T in our memory. And we don't know what compiler and options were used on the other side (maybe the struct on the other side is packed while ours is not).
In short, I see some potential problems with just casting raw char buffers into other types and would try to avoid writing code such as above. But apparently it works and is how "everybody does it".
My question is: is recovering structs sent over a network and received into a char buffer of appropriate size legal according to C++17 standard?
If not, what about using std::aligned_storage<sizeof(T), alignof(T)> to receive such structs? And if std::aligned_storage is not legal either, is there any legal way of sending raw structs over a network, or is it a bad idea that just happens to work... until it doesn't?
I view structs as a way of representing data types and treat the way the compiler lays them out in memory is an implementation detail and not as a wire format for data exchange to be relied upon, but I am open to being wrong.
[1] www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4713.pdf
The dicey part is not so much the memory alignment, but rather the lifetime of the T object. When you reinterpret_cast<> memory as a T pointer, that does not create an instance of the object, and using it as if it was would lead to Undefined Behavior.
In C++, all objects have to come into being and stop existing, thus defining their lifetime. That even applies to basic data types like int and float. The only exception to this is char.
In other words, what's legal is to copy the bytes from the buffer into an already existing object, like so:
char buffer[sizeof(T)];
// fill the buffer...
T value;
std::memcpy(&value, buffer, sizeof(T));
Don't worry about performance. The compiler will optimize all that away.
Related
I had an interesting discussion with a guy smarter than me and I remained with an open question about aligned storage and trivially copyable/destructible types.
Consider the following example:
#include <type_traits>
#include <vector>
#include <cassert>
struct type {
using storage_type = std::aligned_storage_t<sizeof(void *), alignof(void *)>;
using fn_type = int(storage_type &);
template<typename T>
static int proto(storage_type &storage) {
static_assert(std::is_trivially_copyable_v<T>);
static_assert(std::is_trivially_destructible_v<T>);
return *reinterpret_cast<T *>(&storage);
}
std::aligned_storage_t<sizeof(void *), alignof(void *)> storage;
fn_type *fn;
bool weak;
};
int main() {
static_assert(std::is_trivially_copyable_v<type>);
static_assert(std::is_trivially_destructible_v<type>);
std::vector<type> vec;
type t1;
new (&t1.storage) char{'c'};
t1.fn = &type::proto<char>;
t1.weak = true;
vec.push_back(t1);
type t2;
new (&t2.storage) int{42};
t2.fn = &type::proto<int>;
t2.weak = false;
vec.push_back(t2);
vec.erase(std::remove_if(vec.begin(), vec.end(), [](const auto &t) { return t.weak; }), vec.end());
assert(vec.size() == 1);
assert(!vec[0].weak);
assert(vec[0].fn(vec[0].storage) == 42);
}
This is a simplified version of a real world case. I really hope I didn't make errors or simplified it too much.
As you can see, the idea is that there exists a type called type (naming things is hard, you know) having three data members:
storage that is a bunch of byte having size sizeof(void *)
fn a pointer to a function having type int(storage_type &)
weak an useless bool used only to introduce the example
To create new instances of type (see the main function), I put a value (either an int or a char) in the storage area and the right specialization of the static function template proto in fn.
Later on, when I want to invoke fn and get the integer value it returns, I do something like this:
int value = type_instance.fn(type_instance.storage);
So far, so good. Despite the fact of being risky and error-prone (but this is an example, the real use case is not), this works.
Note that type and all the types I put in the storage (int and char in the example) are required to be both trivially copyable and trivially destructible. This is also the core of the discussion I had.
The problem (or better, the doubt) arises when I put instances of types eg in a vector (see the main function) and decide to remove one of them from within the array, so that some of the others are moved around to keep it packed.
More in general, I'm no longer that sure about what happens when I want to copy or move instances of type and if it's UB or not.
My guess was that it was allowed being the types put in the storage trivially copyable and trivially destructible. On the other side, I've been told that this isn't directly allowed by the standard and it can be considered a benign UB, because almost all the compilers in fact allows you to do that (I can guarantee this, it seemes to work everywhere for some definitions of work).
So, the question is: is this allowed or UB and what can I do to work around the issue in the second case? Moreover, is C++20 going to change things for that?
This problem reduces to basically what LanguageLawyer suggested:
alignas(int) unsigned char buff1[sizeof(int)];
alignas(int) unsigned char buff2[sizeof(int)];
new (buff1) int {42};
std::memcpy(buff2, buff1, sizeof(buff1));
assert(*std::launder(reinterpret_cast<int*>(buff2)) == 42); // is it ok?
In other words - when I copy bytes around, do I also copy around "object-ness"? buff1 is certainly providing storage for an int - when we copy those bytes, does buff2 also now provide storage for an int?
And the answer is... no. There are exactly four ways to create an object, per [intro.object]:
An object is created by a definition, by a new-expression ([expr.new]), when implicitly changing the active member of a union, or when a temporary object is created ([conv.rval], [class.temporary]).
None of those things happened here, so we don't have an object in buff2 of any kind (outside of just the normal array of unsigned char), hence behavior is undefined. Simply put, memcpy does not create objects.
In the original example, it's only the 3rd line that requires that implicit object creation:
assert(vec.size() == 1); // ok
assert(!vec[0].weak); // ok
assert(vec[0].fn(vec[0].storage) == 42); // UB
This is why P0593 exists and has a special section for memmove/memcpy:
A call to memmove behaves as if it
copies the source storage to a temporary area
implicitly creates objects in the destination storage, and then
copies the temporary storage to the destination storage.
This permits memmove to preserve the types of trivially-copyable objects, or to be used to reinterpret a byte representation of one object as that of another object.
This is what you need here - that implicit object creation step is currently missing from C++ today.
That said, you can more or less rely on this "doing the right thing" given the simply enormous body of C++ code that exists today relies on this code to "just work."
The Multiboot Specification has structures like this:
struct multiboot_tag_mmap
{
multiboot_uint32_t type;
multiboot_uint32_t size;
multiboot_uint32_t entry_size;
multiboot_uint32_t entry_version;
struct multiboot_mmap_entry entries[0];
};
The intent seems to be that the array size can vary. The information is not known until passed along by the boot loader. In hosted C++, the suggested advice is to "use vector". Well, I can't do that. The alternative is to use dynamic allocation, but that would require implementing a significant chunk of the kernel (paging, MMU, etc.) before I even have the memory map information. A bit of a chicken or egg problem.
The "hack" is to just enable extensions with gnu++11. But I try to avoid using extensions as much as possible to avoid C-ish code or code that could potentially lead to undefined behavior. The more portable the code is, the less chance of bugs in my opinion.
Finally, you iterate over the memory map like this:
for (mmap = ((struct multiboot_tag_mmap *) tag)->entries;
(multiboot_uint8_t *) mmap
< (multiboot_uint8_t *) tag + tag->size;
mmap = (multiboot_memory_map_t *)
((unsigned long) mmap
+ ((struct multiboot_tag_mmap *) tag)->entry_size))
So the size of the structure is tag->size.
I can modify the multiboot header so long as the semantics are the same. The point is how it looks to the bootloader. What can I do?
Instead of 0-sized array, you can use 1-sized array:
struct multiboot_tag_mmap
{
...
struct multiboot_mmap_entry entries[1];
};
This will change only result of sizeof(struct multiboot_tag_mmap), but it shouldn't be used in any case: size of allocated structure should be computed as
offsetof(struct multiboot_tag_mmap, entries) + <num-of-entries> * sizeof(struct multiboot_mmap_entry)
Alignment of the map structure doesn't depends on the number of elements in the entries array, but on the entry type.
Strictly confirming alternative:
If there is known bounary for array size, one can use this boundary for type declaration:
struct multiboot_tag_mmap
{
...
struct multiboot_mmap_entry entries[<UPPER-BOUNDARY>];
};
For such declaration all possible issues described below are not applied.
NOTE about elements accessing:
For accessing elements (above the first one) in such flexible array one need to declare new pointer variable:
struct multiboot_mmap_entry* entries = tag->entries;
entries[index] = ...; // This is OK.
instead of using entries field directly:
tag->entries[index] = ...; // WRONG! May spuriously fail!
The thing is that compiler, knowing that the only one element exists in the entries field array, may optimize last case it to:
tag->entries[0] = ...; // Compiler is in its rights to assume index to have the only allowed value
Issues about standard confirmance: With flexible array approach, there is no object of type struct multiboot_tag_mmap in the memory(in the heap or on the stack). All we have is a pointer of this type, which is never dereferenced (e.g. for making full copy of the object). Similarly, there is no object of the array type struct multiboot_mmap_entry[1], corresponded to the entries field of the structure, this field is used only for conversion to generic pointer of type struct multiboot_mmap_entry*.
So, phrase in the C standard, which denotes Undefine Behavior
An object is assigned to an inexactly overlapping object or to an exactly overlapping object with incompatible type
is inapplicable for accessing entries array field using generic pointer: there is no overlapping object here.
The answers I got for this question until now has two exactly the opposite kinds of answers: "it's safe" and "it's undefined behaviour". I decided to rewrite the question in whole to get some better clarifying answers, for me and for anyone who might arrive here via Google.
Also, I removed the C tag and now this question is C++ specific
I am making an 8-byte-aligned memory heap that will be used in my virtual machine. The most obvious approach that I can think of is by allocating an array of std::uint64_t.
std::unique_ptr<std::uint64_t[]> block(new std::uint64_t[100]);
Let's assume sizeof(float) == 4 and sizeof(double) == 8. I want to store a float and a double in block and print the value.
float* pf = reinterpret_cast<float*>(&block[0]);
double* pd = reinterpret_cast<double*>(&block[1]);
*pf = 1.1;
*pd = 2.2;
std::cout << *pf << std::endl;
std::cout << *pd << std::endl;
I'd also like to store a C-string saying "hello".
char* pc = reinterpret_cast<char*>(&block[2]);
std::strcpy(pc, "hello\n");
std::cout << pc;
Now I want to store "Hello, world!" which goes over 8 bytes, but I still can use 2 consecutive cells.
char* pc2 = reinterpret_cast<char*>(&block[3]);
std::strcpy(pc2, "Hello, world\n");
std::cout << pc2;
For integers, I don't need a reinterpret_cast.
block[5] = 1;
std::cout << block[5] << std::endl;
I'm allocating block as an array of std::uint64_t for the sole purpose of memory alignment. I also do not expect anything larger than 8 bytes by its own to be stored in there. The type of the block can be anything if the starting address is guaranteed to be 8-byte-aligned.
Some people already answered that what I'm doing is totally safe, but some others said that I'm definitely invoking undefined behaviour.
Am I writing correct code to do what I intend? If not, what is the appropriate way?
The global allocation functions
To allocate an arbitrary (untyped) block of memory, the global allocation functions (§3.7.4/2);
void* operator new(std::size_t);
void* operator new[](std::size_t);
Can be used to do this (§3.7.4.1/2).
§3.7.4.1/2
The allocation function attempts to allocate the requested amount of storage. If it is successful, it shall return the address of the start of a block of storage whose length in bytes shall be at least as large as the requested size. There are no constraints on the contents of the allocated storage on return from the allocation function. The order, contiguity, and initial value of storage allocated by successive calls to an allocation function are unspecified. The pointer returned shall be suitably aligned so that it can be converted to a pointer of any complete object type with a fundamental alignment requirement (3.11) and then used to access the object or array in the storage allocated (until the storage is explicitly deallocated by a call to a corresponding deallocation function).
And 3.11 has this to say about a fundamental alignment requirement;
§3.11/2
A fundamental alignment is represented by an alignment less than or equal to the greatest alignment supported by the implementation in all contexts, which is equal to alignof(std::max_align_t).
Just to be sure on the requirement that the allocation functions must behave like this;
§3.7.4/3
Any allocation and/or deallocation functions defined in a C++ program, including the default versions in the library, shall conform to the semantics specified in 3.7.4.1 and 3.7.4.2.
Quotes from C++ WD n4527.
Assuming the 8-byte alignment is less than the fundamental alignment of the platform (and it looks like it is, but this can be verified on the target platform with static_assert(alignof(std::max_align_t) >= 8)) - you can use the global ::operator new to allocate the memory required. Once allocated, the memory can be segmented and used given the size and alignment requirements you have.
An alternative here is the std::aligned_storage and it would be able to give you memory aligned at whatever the requirement is.
typename std::aligned_storage<sizeof(T), alignof(T)>::type buffer[100];
From the question, I assume here that the both the size and alignment of T would be 8.
A sample of what the final memory block could look like is (basic RAII included);
struct DataBlock {
const std::size_t element_count;
static constexpr std::size_t element_size = 8;
void * data = nullptr;
explicit DataBlock(size_t elements) : element_count(elements)
{
data = ::operator new(elements * element_size);
}
~DataBlock()
{
::operator delete(data);
}
DataBlock(DataBlock&) = delete; // no copy
DataBlock& operator=(DataBlock&) = delete; // no assign
// probably shouldn't move either
DataBlock(DataBlock&&) = delete;
DataBlock& operator=(DataBlock&&) = delete;
template <class T>
T* get_location(std::size_t index)
{
// https://stackoverflow.com/a/6449951/3747990
// C++ WD n4527 3.9.2/4
void* t = reinterpret_cast<void*>(reinterpret_cast<unsigned char*>(data) + index*element_size);
// 5.2.9/13
return static_cast<T*>(t);
// C++ WD n4527 5.2.10/7 would allow this to be condensed
//T* t = reinterpret_cast<T*>(reinterpret_cast<unsigned char*>(data) + index*element_size);
//return t;
}
};
// ....
DataBlock block(100);
I've constructed more detailed examples of the DataBlock with suitable template construct and get functions etc., live demo here and here with further error checking etc..
A note on the aliasing
It does look like there are some aliasing issues in the original code (strictly speaking); you allocate memory of one type and cast it to another type.
It may probably work as you expect on your target platform, but you cannot rely on it. The most practical comment I've seen on this is;
"Undefined behaviour has the nasty result of usually doing what you think it should do, until it doesn’t” - hvd.
The code you have probably will work. I think it is better to use the appropriate global allocation functions and be sure that there is no undefined behaviour when allocating and using the memory you require.
Aliasing will still be applicable; once the memory is allocated - aliasing is applicable in how it is used. Once you have an arbitrary block of memory allocated (as above with the global allocation functions) and the lifetime of an object begins (§3.8/1) - aliasing rules apply.
What about std::allocator?
Whilst the std::allocator is for homogenous data containers and what your are looking for is akin to heterogeneous allocations, the implementation in your standard library (given the Allocator concept) offers some guidance on raw memory allocations and corresponding construction of the objects required.
Update for the new question:
The great news is there's a simple and easy solution to your real problem: Allocate the memory with new (unsigned char[size]). Memory allocated with new is guaranteed in the standard to be aligned in a way suitable for use as any type, and you can safely alias any type with char*.
The standard reference, 3.7.3.1/2, allocation functions:
The pointer returned shall be suitably aligned so that it can be
converted to a pointer of any complete object type and then used to
access the object or array in the storage allocated
Original answer for the original question:
At least in C++98/03 in 3.10/15 we have the following which pretty clearly makes it still undefined behavior (since you're accessing the value through a type that's not enumerated in the list of exceptions):
If a program attempts to access the stored value of an object through
an lvalue of other than one of the following types the behavior is
undefined):
— the dynamic type of the object,
— a cvqualified version of the dynamic type of the object,
— a type that is the signed or unsigned type corresponding to the dynamic type of the object,
— a type that is the signed or unsigned type corresponding to a cvqualified version of the dynamic type of the object,
— an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union),
— a type that is a (possibly cvqualified) base class type of the dynamic type of the object,
— a char or unsigned char type.
pc pf and pd are all different types that access memory specified in block as uint64_t, so for say 'pf the shared types are float and uint64_t.
One would violate the strict aliasing rule were once to write using one type and read using another since the compile could we reorder the operations thinking there is no shared access. This is not your case however, since the uint64_t array is only used for assignment, it is exactly the same as using alloca to allocate the memory.
Incidentally there is no issue with the strict aliasing rule when casting from any type to a char type and visa versa. This is a common pattern used for data serialization and deserialization.
A lot of discussion here and given some answers that are slightly wrong, but making up good points, I just try to summarize:
exactly following the text of the standard (no matter what version) ... yes, this is undefined behaviour. Note the standard doesn't even have the term strict aliasing -- just a set of rules to enforce it no matter what implementations could define.
understanding the reason behind the "strict aliasing" rule, it should work nicely on any implementation as long as neither float or double take more than 64 bits.
the standard won't guarantee you anything about the size of float or double (intentionally) and that's the reason why it is that restrictive in the first place.
you can get around all this by ensuring your "heap" is an allocated object (e.g. get it with malloc()) and access the aligned slots through char * and shifting your offset by 3 bits.
you still have to make sure that anything you store in such a slot won't take more than 64 bits. (that's the hard part when it comes to portability)
In a nutshell: your code should be safe on any "sane" implementation as long as size constraints aren't a problem (means: the answer to the question in your title is most likely no), BUT it's still undefined behaviour (means: the answer to your last paragraph is yes)
I'll make it short: All your code works with defined semantics if you allocate the block using
std::unique_ptr<char[], std::free>
mem(static_cast<char*>(std::malloc(800)));
Because
every type is allowed to alias with a char[] and
malloc() is guaranteed to return a block of memory sufficiently aligned for all types (except maybe SIMD ones).
We pass std::free as a custom deleter, because we used malloc(), not new[], so calling delete[], the default, would be undefined behaviour.
If you're a purist, you can also use operator new:
std::unique_ptr<char[]>
mem(static_cast<char*>(operator new[](800)));
Then we don't need a custom deleter. Or
std::unique_ptr<char[]> mem(new char[800]);
to avoid the static_cast from void* to char*. But operator new can be replaced by the user, so I'm always a bit wary of using it. OTOH; malloc cannot be replaced (only in platform-specific ways, such as LD_PRELOAD).
Yes, because the memory locations pointed to by pf could overlap depending on the size of float and double. If they didn't, then the results of reading *pd and *pf would be well defined but not the results of reading from block or pc.
The behavior of C++ and the CPU are distinct. Although the standard provides memory suitable for any object, the rules and optimizations imposed by the CPU make the alignment for any given object "undefined" - an array of short would reasonably be 2 byte aligned, but an array of a 3 byte structure may be 8 byte aligned. A union of all possible types can be created and used between your storage and the usage to ensure no alignment rules are broken.
union copyOut {
char Buffer[200]; // max string length
int16 shortVal;
int32 intVal;
int64 longIntVal;
float fltVal;
double doubleVal;
} copyTarget;
memcpy( copyTarget.Buffer, Block[n], sizeof( data ) ); // move from unaligned space into union
// use copyTarget member here.
If you tag this as C++ question,
(1) why use uint64_t[] but not std::vector?
(2) in term of memory management, your code lack of management logic, which should keep track of which blocks are in use and which are free and the tracking of contiguoous blocks, and of course the allocate and release block methods.
(3) the code shows an unsafe way of using memory. For example, the char* is not const and therefore the block can be potentially be written to and overwrite the next block(s). The reinterpret_cast is consider danger and should be abstract from the memory user logic.
(4) the code doesn't show the allocator logic. In C world, the malloc function is untyped and in C++ world, the operator new is typed. You should consider something like the new operator.
Or even better a template <T*>?
In case the memory mapped file contains a sequence of 32 bit integers, if data() returned a void*, we could be able to static cast to std::uint32_t directly.
Why did boost authors choose to return a char* instead?
EDIT: as pointed out, in case portability is an issue, a translation is needed. But saying that a file (or a chunk of memory in this case) is a stream of bytes more than it is a stream of bits, or of IEEE754 doubles, or of complex data structures, seems to me a very broad statement that needs some more explanation.
Even having to handle endianness, being able to directly map to a vector of be_uint32_t as suggested (and as implemented here) would make the code much more readable:
struct be_uint32_t {
std::uint32_t raw;
operator std::uint32_t() { return ntohl(raw); }
};
static_assert(sizeof(be_uint32_t)==4, "POD failed");
Is it allowed/advised to cast to a be_uint32_t*? Why, or why not?
Which kind of cast should be used?
EDIT2: Since it seems difficult to get to the point instead of discussing weather the memory model of an elaborator is made of bits, bytes or words I will rephrase giving an example:
#include <cstdint>
#include <memory>
#include <vector>
#include <iostream>
#include <boost/iostreams/device/mapped_file.hpp>
struct entry {
std::uint32_t a;
std::uint64_t b;
} __attribute__((packed)); /* compiler specific, but supported
in other ways by all major compilers */
static_assert(sizeof(entry) == 12, "entry: Struct size mismatch");
static_assert(offsetof(entry, a) == 0, "entry: Invalid offset for a");
static_assert(offsetof(entry, b) == 4, "entry: Invalid offset for b");
int main(void) {
boost::iostreams::mapped_file_source mmap("map");
assert(mmap.is_open());
const entry* data_begin = reinterpret_cast<const entry*>(mmap.data());
const entry* data_end = data_begin + mmap.size()/sizeof(entry);
for(const entry* ii=data_begin; ii!=data_end; ++ii)
std::cout << std::hex << ii->a << " " << ii->b << std::endl;
return 0;
}
Given that the map file contains the bit expected in the correct order, is there any other reason to avoid using the reinterpret_cast to use my virtual memory without copying it first?
If there is not, why force the user to do a reinterpret_cast by returning a typed pointer?
Please answer all the questions for bonus points :)
In case the memory mapped file contains a sequence of 32 bit integers, if data() returned a void*, we could be able to static cast to std::uint32_t directly.
No, not really. You still have to consider (if nothing else) endianness. This "one step conversion" idea would lead you into a false sense of security. You're forgetting about an entire layer of translation between the bytes in the file and the 32-bit integer you want to get into your program. Even when that translation happens to be a no-op on your present system and for a given file, it's still a translation step.
It's much better to get an array of bytes (literally what a char* points to!) then you know you have to do some thinking to ensure that your pointer conversion is valid and that you are performing whatever other work is required.
char* represents array of raw bytes, which is what mapped_file::data is in most general case.
void* would be misleading as it provides less information about the contained type and requires more setup to work with then char* - we know that file contents are some bytes, which char* represents.
Template return type would require conversion to that type be performed inside the library, while it makes more sense to do that on the caller side (since library just provides an interface to raw file contents, and the caller knows specifically what those contents are).
Returning a char * seems to be just a (peculiar) design decision of boost::iostreams implementation.
Other APIs like e.g. the boost interprocess return void*.
As observed by sehe the UNIX mmap specification (and malloc) use void* as well.
It is somewhat a duplicate of void* or char* for generic buffer representation?
As a note of caution the layer of translation mentioned by Lightness in another answer may be needed when the memory is written from one architecture and read on a different one. Endianness is easy to solve using a conversion type, but alignment need to be considered as well.
About static cast: http://en.cppreference.com/w/cpp/language/static_cast mentions:
A prvalue of type pointer to void (possibly cv-qualified) can be
converted to pointer to any type. If the value of the original pointer
satisfies the alignment requirement of the target type, then the
resulting pointer value is unchanged, otherwise it is unspecified.
Conversion of any pointer to pointer to void and back to pointer to
the original (or more cv-qualified) type preserves its original value.
So if the file to be memory mapped was created on a different architecture with a different alignment, the loading may fail (e.g. with a SIGBUS) depending on the architecture and the OS.
Can this potentially cause undefined behaviour?
uint8_t storage[4];
// We assume storage is properly aligned here.
int32_t* intPtr = new((void*)storage) int32_t(4);
// I know this is ok:
int32_t value1 = *intPtr;
*intPtr = 5;
// But can one of the following cause UB?
int32_t value2 = reinterpret_cast<int32_t*>(storage)[0];
reinterpret_cast<int32_t*>(storage)[0] = 5;
char has special rules for strict-aliasing. If I use char instead of uint8_t is it still Undefined Behavior? What else changes?
As member DeadMG pointed out, reinterpret_cast is implementation dependent. If I use a C-style cast (int32_t*)storage instead, what would change?
The pointer returned by placement new can be just as UB-causing as any other pointer when aliasing considerations are brought into it. It's your responsibility to ensure that the memory you placed the object into isn't aliased by anything it shouldn't be.
In this case, you cannot assume that uint8_t is an alias for char and therefore has the special aliasing rules applied. In addition, it would be fairly pointless to use an array of uint8_t rather than char because sizeof() is in terms of char, not uint8_t. You'd have to compute the size yourself.
In addition, reinterpret_cast's effect is entirely implementation-defined, so the code certainly does not have a well-defined meaning.
To implement low-level unpleasant memory hacks, the original memory needs to be only aliased by char*, void*, and T*, where T is the final destination type- in this case int, plus whatever else you can get from a T*, such as if T is a derived class and you convert that derived class pointer to a pointer to base. Anything else violates strict aliasing and hello nasal demons.
Your version using the usual placement new is indeed fine.
There is an interpretation1 of §§ 3.8/1 and 3.8/4 where objects of trivial types are able to ‘vanish’ and ‘appear’ on demand. This not a free pass that allows disregarding aliasing rules, so notice:
std::uint16_t storage[2];
static_assert( /* std::uint16_t is not a character type */ );
static_assert( /* storage is properly aligned for our purposes */ );
auto read = *reinterpret_cast<std::uint32_t*>(&storage);
// At this point either we’re attempting to read the value of an
// std::uint16_t object through an std::uint32_t glvalue, a clear
// strict aliasing violation;
// or we’re reading the indeterminate value of a new std::uint32_t
// object freshly constructed in the same storage without effort
// on our part
If on the other hand you swapped the casts around in your second snippet (i.e. reinterpret and write first), you’re not entirely safe either. While under the interpretation you can justify the write to happen on a new std::uint32_t object that reuses the storage implicitly, the subsequent read is of the form
auto value2 = *reinterpret_cast<int32_t*>(storage);
and §3.8/5 says (emphasis mine and extremely relevant):
[…] after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. […] such a pointer refers to allocated storage (3.7.4.2), and using the pointer as if the pointer were of type void*, is well-defined.
§3.8/6 is the same but in reference/glvalue form (arguably more relevant since we’re reusing a name and not a pointer here, but the paragraph is imo harder to understand out of context). Also see §3.8/7, which gives some limited leeway that I don’t think applies in your case.
To make things simpler, the remaining problem is this:
T object;
object.~T();
new (&object) U_thats_really_different_from_T;
&object; // Is this allowed? What does it mean?
static_cast<void*>(&object); // Is this?
As it so happens if the type of the storage happens to involve a plain or unsigned character type (e.g. your storage really has type unsigned char[4]) then I’d say you have a basis to justify forming a pointer/reference to the storage of the new object (possibly to be reinterpreted later). See e.g. ¶¶ 5 and 6 again, which have an explicit escape clause for forming a pointer/reference/glvalue and §1.8 The C++ object model that describes how an object involves a constituent array of bytes. The rules governing the pointer conversions should be straightforward and uncontroversial (at least by comparison…).
1: it’s hard to gauge how well this interpretation is received in the community — I’ve seen it on the Boost mailing list, where there was some scepticism towards it