Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it legal to reuse memory from a fundamental type array for a different (yet still fundamental) type array

This is a follow up to this other question about memory re-use. As the original question was about a specific implementation, the answer was related to that specific implementation.

So I wonder whether, it is legal in a conformant implementation to re-use the memory of an array of a fundamental type for an array of a different type provided:

  • both types are fundamental type and as such have trivial dtor and default ctor
  • both types have same size and alignment requirement

I ended with the following example code:

#include <iostream>

constexpr int Size = 10;

void *allocate_buffer() {
    void * buffer = operator new(Size * sizeof(int), std::align_val_t{alignof(int)});
    int *in = reinterpret_cast<int *>(buffer); // Defined behaviour because alignment is ok
    for (int i=0; i<Size; i++) in[i] = i;  // Defined behaviour because int is a fundamental type:
                                           // lifetime starts when is receives a value
    return buffer;
}
int main() {
    void *buffer = allocate_buffer();        // Ok, defined behaviour
    int *in = static_cast<int *>(buffer);    // Defined behaviour since the underlying type is int *
    for(int i=0; i<Size; i++) {
        std::cout << in[i] << " ";
    }
    std::cout << std::endl;
    static_assert(sizeof(int) == sizeof(float), "Non matching type sizes");
    static_assert(alignof(int) == alignof(float), "Non matching alignments");
    float *out = static_cast<float *>(buffer); //  (question here) Declares a dynamic float array starting at buffer
    // std::cout << out[0];      // UB! object at &out[0] is an int and not a float
    for(int i=0; i<Size; i++) {
        out[i] = static_cast<float>(in[i]) / 2;  // Defined behaviour, after execution buffer will contain floats
                                                 // because float is a fundamental type and memory is re-used.
    }
    // std::cout << in[0];       // UB! lifetime has ended because memory has been reused
    for(int i=0; i<Size; i++) {
        std::cout << out[i] << " ";         // Defined behaviour since the actual object type is float *
    }
    std::cout << std::endl;
    return 0;
}

I have added comments explaining why I think that this code should have defined behaviour. And IMHO everything is fine and AFAIK standard conformant, but I was not able to find whether the line marked question here is or not valid.

Float objects do re-use memory from int objects, so life time of the ints end when life time of the floats start, so the stric-aliasing rule should not be a problem. Array was dynamically allocated so objects (int and floats) are in fact all created in a void type array returned by operator new. So I think that everything should be ok.

But as it allows for low level object replacement which is normally frowned upon in modern C++ I must acknowledge I have a doubt...

So the question is: does above code invokes UB and if yes where and why?

Disclaimer: I would advise against this code in a portable code base, and this is really a language lawyer question.

like image 674
Serge Ballesta Avatar asked Aug 20 '18 13:08

Serge Ballesta


People also ask

What are the data types supported by an array?

The array supports multiple data types to represent a collection of data. Some of them include char, integer, float, numeric, binary, etc. 4. Rephrase data value

What is an array?

Definition, Types & Usage An array is a collection of homogeneous elements stored in a contiguous memory location for better access and easier calculation by the system. Contrary to scalar variables, an array contains a sequence of multiple elements that may range from text, integers, or even other sets of arrays.

What are the characteristics of a fixed array?

Arrays are usually of a fixed size and do not allow deletion or addition of data. It contains a fixed number of single data type values. The length of an array becomes fixed once it is created. 2. No random access Data elements cannot be randomly accessed.

What types of data types are supported by Raml?

Below you can see data types that are supported by Raml 1.0. We have a group of scalar types like integer, date-only, and so on. We have also compound structures like arrays and objects. XSD and JSON schema is a separate type as developers can still share custom types using these schemas.


Video Answer


2 Answers

int *in = reinterpret_cast<int *>(buffer); // Defined behaviour because alignment is ok

Correct. But probably not in the sense you'd expect. [expr.static.cast]

A prvalue of type “pointer to cv1 void” can be converted to a prvalue of type “pointer to cv2 T”, where T is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. If the original pointer value represents the address A of a byte in memory and A does not satisfy the alignment requirement of T, then the resulting pointer value is unspecified. Otherwise, if the original pointer value points to an object a, and there is an object b of type T (ignoring cv-qualification) that is pointer-interconvertible with a, the result is a pointer to b. Otherwise, the pointer value is unchanged by the conversion.

There is no int nor any pointer-interconvertible object at buffer, therefore the pointer value is unchanged. in is a pointer of type int* that points to a region of raw memory.

for (int i=0; i<Size; i++) in[i] = i;  // Defined behaviour because int is a fundamental type:
                                       // lifetime starts when is receives a value

Is incorrect. [intro.object]

An object is created by a definition, by a new-expression, when implicitly changing the active member of a union, or when a temporary object is created.

Noticeably absent is assignment. No int is created. In fact, by elimination, in is an invalid pointer, and dereferencing it is UB.

The later float* all also follows as UB.

Even in absence of all the aforementioned UB by proper use of new (pointer) Type{i}; to create objects, there is no array object in existence. The (unrelated) objects just happens to be side by side in memory. This means pointer arithmetic with the resulting pointer is also UB. [expr.add]

When an expression that has integral type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the expression P points to element x[i] of an array object x with n elements, the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) element x[i+j] if 0 ≤ i+j ≤ n; otherwise, the behavior is undefined. Likewise, the expression P - J points to the (possibly-hypothetical) element x[i−j] if 0 ≤ i−j ≤ n; otherwise, the behavior is undefined.

Where hypothetical element refers to the one past-the-end (hypothetical) element. Note that a pointer to a one past-the-end element that happens to be at the same address location as another object doesn't point to that other object.

like image 141
Passer By Avatar answered Oct 13 '22 07:10

Passer By


Passer By's answer covers why the example program has undefined behaviour. I'll attempt to answer how to reuse storage without UB with minimal UB (reuse of storage for arrays is technically impossible in standard C++ given the current wording of the standard, so to achieve reuse, the programmer has to rely on the implementation to "do the right thing").

Converting a pointer does not automatically manifest objects into being. You have to first construct the float objects. This starts their lifetime and ends the lifetime of the int objects (for non-trivial objects, destructor would need to be called first):

for(int i=0; i<Size; i++)
    new(in + i) float;

You can use the pointer returned by placement new (which is discarded in my example) directly to use the freshly constructed float objects, or you can std::launder the buffer pointer:

float *out = std::launder(reinterpret_cast<float*>(buffer));

However, it is much more typical to reuse the storage of type unsigned char (or std::byte) rather than storage of int objects.

like image 5
eerorika Avatar answered Oct 13 '22 07:10

eerorika