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 implementat
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 tocv2 T
”, whereT
is an object type andcv2
is the same cv-qualification as, or greater cv-qualification than,cv1
. If the original pointer value represents the addressA
of a byte in memory andA
does not satisfy the alignment requirement ofT
, then the resulting pointer value is unspecified. Otherwise, if the original pointer value points to an objecta
, and there is an objectb
of typeT
(ignoring cv-qualification) that is pointer-interconvertible witha
, the result is a pointer tob
. 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 elementx[i]
of an array objectx
withn
elements, the expressionsP + J
andJ + P
(where J has the value j) point to the (possibly-hypothetical) elementx[i+j] if 0 ≤ i+j ≤ n;
otherwise, the behavior is undefined. Likewise, the expressionP - J
points to the (possibly-hypothetical) elementx[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.
I have just popped in because it felt to me that there is at least one unanswered question, that was not spoken out loud, apologies if that is not true. I think that the guys brilliantly answered the main question of this problem: where and why it is undefined behavior; user2079303 gave few ideas how to fix it. I will try to answer the question how to fix the code and why it is valid. Before starting to read my post, please, read the answers and comment discussions under those answers of Passer By and user2079303.
Basically the issue is that objects do not exist even though they do not really need anything, except the storage, to exist. This is said in the lifetime section of the standard, however, in The C++ object model section before it is stated that
An object is created by a definition (6.1), by a new-expression (8.3.4), when implicitly changing the active member of a union (12.3), or when a temporary object is created (7.4, 15.2).
A little bit tricky definition of object concept, but make sense. The issue is more precisely addressed in proposal Implicit creation of objects for low-level object manipulation to simplify the object model. Until then we should explicitly create an object by mentioned means. One of those that will work, for this case is new-placement expression, new-placement is a non-allocating new-expression, that create an object. For this particular case this will help us to crete the missing array objects and floating objects. The code below shows what I have come up with including some comments and assembly instructions associated with the lines (clang++ -g -O0
was used).
constexpr int Size = 10;
void* allocate_buffer() {
// No alignment required for the `new` operator if your object does not require
// alignment greater than alignof(std::max_align_t), what is the case here
void* buffer = operator new(Size * sizeof(int));
// 400fdf: e8 8c fd ff ff callq 400d70 <operator new(unsigned long)@plt>
// 400fe4: 48 89 45 f8 mov %rax,-0x8(%rbp)
// (was missing) Create array of integers, default-initialized, no
// initialization for array of integers
new (buffer) int[Size];
int* in = reinterpret_cast<int*>(buffer);
// Two line result in a basic pointer value copy
// 400fe8: 48 8b 45 f8 mov -0x8(%rbp),%rax
// 400fec: 48 89 45 f0 mov %rax,-0x10(%rbp)
for (int i = 0; i < Size; i++)
in[i] = i;
return buffer;
}
int main() {
void* buffer = allocate_buffer();
// 401047: 48 89 45 d0 mov %rax,-0x30(%rbp)
// static_cast equivalent in this case to reinterpret_cast
int* in = static_cast<int*>(buffer);
// Static cast results in a pointer value copy
// 40104b: 48 8b 45 d0 mov -0x30(%rbp),%rax
// 40104f: 48 89 45 c8 mov %rax,-0x38(%rbp)
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");
for (int i = 0; i < Size; i++) {
int t = in[i];
// (was missing) Create float with a direct initialization
// Technically that is reuse of the storage of the array, hence that array does
// not exist anymore.
new (in + i) float{t / 2.f};
// No new is called
// 4010e4: 48 8b 45 c8 mov -0x38(%rbp),%rax
// 4010e8: 48 63 4d c0 movslq -0x40(%rbp),%rcx
// 4010ec: f3 0f 2a 4d bc cvtsi2ssl -0x44(%rbp),%xmm1
// 4010f1: f3 0f 5e c8 divss %xmm0,%xmm1
// 4010f5: f3 0f 11 0c 88 movss %xmm1,(%rax,%rcx,4)
// (was missing) Create int array on the same storage, default-initialized, no
// initialization for an array of integers
new (buffer) int[Size];
// No code for new is generated
}
// (was missing) Create float array, default-initialized, no initialization for an array
// of floats
new (buffer) float[Size];
float* out = reinterpret_cast<float*>(buffer);
// Two line result in a simple pointer value copy
// 401108: 48 8b 45 d0 mov -0x30(%rbp),%rax
// 40110c: 48 89 45 b0 mov %rax,-0x50(%rbp)
for (int i = 0; i < Size; i++) {
std::cout << out[i] << " ";
}
std::cout << std::endl;
operator delete(buffer);
return 0;
}
Basically all new-placement expressions are omitted in the machine code even with -O0
. With GCC -O0
operator new
is actually invoked and with -O1
it is omitted as well. Let us forget about formalities of the standard for a second and think straight from practical sense. Why would we need to actually call the functions that are doing nothing, there is nothing that prevents it being a working without those, right? Because C++ is exactly the language where the whole control over the memory is given to the program, not to some runtime libraries or virtual machine, etc. One of the reason I might think here is that the standard again gives compilers more freedom on optimizations restricting the program to some extra action. The idea might have been that the compiler can do whatever reordering, omitting magic with the machine code knowing only definition, new-expression, union, temporary objects as new objects providers that guide the optimization algorithm. Most probably in the reality there are no such optimizations that will screw up your code if you allocated memory and did not call new operator on it for a trivial types. Interesting fact is that those non-allocating versions of new operator
are reserved and not allowed for replacement, may be this is exactly meant to be the simplest forms telling the compiler about a new object.
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.