user3810155
user3810155

Reputation:

What is the correct way to allocate and use an untyped memory block in C++?

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?

Upvotes: 22

Views: 4345

Answers (8)

Mark B
Mark B

Reputation: 96311

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.

Upvotes: 6

Niall
Niall

Reputation: 30624

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.

Upvotes: 15

Marc Mutz - mmutz
Marc Mutz - mmutz

Reputation: 25353

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).

Upvotes: 1

simon
simon

Reputation: 391

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.

Upvotes: -2

mksteve
mksteve

Reputation: 13085

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.

Upvotes: 0

user2371524
user2371524

Reputation:

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)

Upvotes: 1

doron
doron

Reputation: 28932

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.

Upvotes: 1

dbush
dbush

Reputation: 225827

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.

Upvotes: 0

Related Questions