22

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?

Niall
  • 28,102
  • 9
  • 90
  • 124
xiver77
  • 6,073
  • 4
  • 23
  • 58
  • 2
    *Any* access to an object with an incompatible pointer type is a violation of the aliasing rule. – Eugene Sh. Jul 15 '15 at 17:08
  • @xiver77 If you want dynamic memory, then use malloc. It will allocate on the heap and the storage duration is allocated. C has special rules that help with aliasing that apply almost perfectly with your case. – this Jul 15 '15 at 17:40
  • To circumvent this all, just use char* as the "plain" type of your heap and shift indexes by 3 bits... –  Jul 15 '15 at 17:42
  • @FelixPalmen That shows that you clearly don't understand the Standard. C allows for char to alias any type, but not vice-versa. – this Jul 15 '15 at 17:44
  • Aliasing is about reads and writes, not about a declaration, any declaration is fine as long as it is not actually used for memory access -- and I stop "discussing" with you at that point if you still insist on your silly ideas. –  Jul 15 '15 at 17:46
  • @FelixPalmen You can start at chapters 6.5 and 6.2.7 where you can read all about how aliasing, objects, declarations, and types actually matter. – this Jul 15 '15 at 17:50
  • Yes, this is technically a no-no. And it will work exactly as you expect on any compiler worth using. – Lee Daniel Crocker Jul 15 '15 at 17:56
  • Just as a little hint ... why do you think the notion of an *allocated object* was introduced in C99? Does `alloca()` return an *allocated object*? Well, final words here, leaving the answer to you. –  Jul 15 '15 at 18:16
  • @FelixPalmen Really classy, introducing a straw-man argument. A function returning allocated duration will obviously cause different behavior, than the automatic duration of variables OP presented. And storage automatic duration is what is being discussed here. – this Jul 15 '15 at 23:04

8 Answers8

15

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.

Community
  • 1
  • 1
Niall
  • 28,102
  • 9
  • 90
  • 124
  • 1
    cleanest solution out here! See also: http://stackoverflow.com/questions/4941793/new-operator-for-memory-allocation-on-heap which summarizes its usage nicely. – Alexander Oh Jul 17 '15 at 20:38
5

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.

Community
  • 1
  • 1
Mark B
  • 91,641
  • 10
  • 102
  • 179
  • 1
    Most of this is C++ specific. What about C? – dbush Jul 15 '15 at 17:24
  • Is declaring an array (which just reserves memory of unspecified content) already a "stored object"? In my interpretation, the first "stored object" here is the character `'\n'` and the array declaration is just a (silly) form of stack allocation, as long as its identifier isn't used for accessing its contents. Correct me if I'm wrong. –  Jul 15 '15 at 17:24
  • 1
    "a type that's not enumerated in the list of exceptions". In C99 at least, there is another source of exceptions listed in the previous paragraph (6.5.6 in one of the standards). Basically, a write to data with no *declared* type can be used to legally change the *effective* type of the object. So the type can legally change type many times during it's lifetime. Just don't write with one type, then read with another. That mightn't apply here, but it means we have to be more careful – Aaron McDaid Jul 21 '15 at 16:59
2

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 like your point about drawing a distinction between the standard and what happens in any sane implementation. I suspect all the implementations are implicitly following a more sensible definition of aliasing. I suspect that we can predictably read through any type, as long as: 1) the most recent write was of the same type (or of type `char[]`), 2) the alignment is respected, and 3) the write was not to a special read-only area reserved for `const` objects. *If* implementers confirm they do this, then we could 'grandfather' it into the standards. This would be a great improvement, I think. – Aaron McDaid Jul 21 '15 at 17:10
  • No unfortunately it's not quite like this. Your point 1) might fail because of reordering, the reason for *strict aliasing* in the first place. But what the OP wants to do shouldn't fail, because an aliasing pointer isn't used in his code. The *declared type* isn't important in this case, but any change to the standard allowing such things would be full of constraints (think about size of types that aren't defined by the standard), so it would probably become very commplicated. –  Jul 21 '15 at 21:55
  • It's true that the OP's code could be reordered in crazy ways under my (vague) reinterpretation of the standard, but it also suffers under the current standard. But if some `memmove`s (or another form of write) were added to the OP's code, then it would no longer suffer from alias confusion (under my interpretation): [Demo on ideone](http://ideone.com/Vp2Pah) - this particular example code is a bit messy, but the `memmove`s make it obvious to the compiler that it can't optimize much. And the memmove "do nothing" as the source and destination are identical (provably so, will be optimized out). – Aaron McDaid Jul 22 '15 at 07:33
  • In simpler terms, there is already language in the C99 standard (yes, the OP is asking about C++, but it's still somewhat relevant) that allows the *effective type* of any object to change at any time, simply through writing. This "changeability" is currently limited only objects with "no declared type". I would extend that to any object (subject to alignment perhaps). This simple change improve things a lot. And it is, I think, consistent with what compilers already do, so it shouldn't really be a problem.. – Aaron McDaid Jul 22 '15 at 07:37
  • @AaronMcDaid simply setting the *effective type* by writing defeats the concept, reordering would still be a problem. BUT I would suggest that the first write access to allocated memory sets the *effective type*, this would probably introduce no changes in currently conforming implementations, as they do not reorder reads and writes through pointers of the same type. –  Jul 22 '15 at 08:37
  • Maybe you already know this, but the effective type of an object that has no declared type can change *multiple* times in (C99). Despite this, and with good code, it is very possible to avoid any reordering issue. That fact that the compiler will respect all accesses through pointers of the same type is sufficient. Even when the careful programmer knows there is type punning going on and writes carefully for it. To clarify: the compiler "knows" that pointer of different type can't/shouldn't alias the same data; however that does not pose a problem to my approach. – Aaron McDaid Jul 22 '15 at 09:12
  • (Ignore my last comment). [This code on ideone](http://ideone.com/D4sV6O) fully describes my intention - it also includes a long wall of text. That text will be my final word. Basically, *skilful* use of `memmove` will make a program uniquely-defined, even in the context of a compiler that is making very aggressive use of assumptions around aliasing. So, yes, even if the compiler assumes that a write through `T*` cannot by relevant for any reads through `U*` (whether before or after in the code), then my use of `memmove` will still save the day. Anyway, thanks for your time thus far! – Aaron McDaid Jul 22 '15 at 10:04
1

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.

doron
  • 24,882
  • 9
  • 58
  • 93
1

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

  1. every type is allowed to alias with a char[] and
  2. 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).

Marc Mutz - mmutz
  • 22,883
  • 10
  • 72
  • 86
0

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.

dbush
  • 162,826
  • 18
  • 167
  • 209
  • Well, in theory you're right, so I don't understand why this is downvoted -- but it should be commented: the `pc` pointer is fine, too, as long as it's used for single characters OR a char array of max 8 bytes OR a string of max 7 characters. For the sizes of `float` and `double`: yes, there's no guarantee. Do you know an implementation using more than 64bit for `double`? –  Jul 15 '15 at 17:20
  • @FelixPalmen Alignment, overlap, is irrelevant. The "theory" states that aliasing is undefined for those types. Full stop. – this Jul 15 '15 at 17:22
  • "Those types" aren't aliased here as long as `block` isn't used for accessing the contents. The only concern making this UB is that there's no guarantee for the sizes of `float` and `double`. So, dbush is right and you are (partly) wrong. –  Jul 15 '15 at 17:27
  • @FelixPalmen No Felix you are Wrong. block is an object and its declared type is uint64_t. You cannot change the declared type of an automatic object. When you read from it using a pointer that isn't uint64_t you are causing undefined behavior because the type of that object is still uint64_t, regardless of *any* unspeakable acts that could have happened to that object before that read. – this Jul 15 '15 at 17:38
  • 2
    Oh my. Declaration doesn't matter, it's reads and writes that do. The declaration doesn't cause any reads or writes, it just reserves memory. Stop behaving like in kindergarten. –  Jul 15 '15 at 17:40
  • @FelixPalmen I'm interpreting the Standard, you are apparently using your intuition. – this Jul 15 '15 at 17:43
  • @FelixPalmen And you you can read the Standard sometimes. Seriously, read it and apply some criticism to your blind beliefs. – this Jul 15 '15 at 17:45
  • 1
    Calm down people. I'm not qualified to help with this particular problem, I think. But I can ask people to be more careful generally. In C99, for example, people often interpret 6.5.7 as the bit that makes everything undefined. But, the previous section, 6.5.6, really helps to narrow the extent of 6.5.7. Basically, don't be too quick to dismiss everything as a violation of the strict aliasing rule, without considering all relevant bits of the standard – Aaron McDaid Jul 21 '15 at 16:55
0

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.
mksteve
  • 11,552
  • 3
  • 24
  • 45
-3

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.

simon
  • 371
  • 1
  • 9
  • 1
    I think you missed the point of my question. The code I showed is not real application level code, it is just an example to explain what my question is. – xiver77 Jul 21 '15 at 14:35
  • Of course it is not real production code. The points of my comment is the posted code is far from being close to let people understand your approach of memory management, and all my points are the basis for creating a memory management library. Yes, untyped memory blocks are the basis of memory management, but there are lots more. – simon Jul 21 '15 at 22:15