Is reusing a memory location safe?
Solution 1:
It's "OK", it works as you have written it (assuming primitives and plain-old-datatypes (PODs)). It is safe. It is effectively a custom memory manager.
Some notes:
-
If objects with non-trivial destructors are created in the location of the allocated memory, make sure it is called
obj->~obj();
-
If creating objects, consider the placement new syntax over a plain cast (works with PODs as well)
Object* obj = new (data) Object();
Check for a
nullptr
(orNULL
), ifmalloc
fails,NULL
is returned- Alignment shouldn't a problem, but always be aware of it when creating a memory manager and make sure that the alignment is appropriate
Given you are using a C++ compiler, unless you want to keep the "C" nature to the code you can also look to the global operator new()
.
And as always, once done don't forget the free()
(or delete
if using new
)
You mention that you are not going to convert any of the code just yet; but if or when you do consider it, there are a few idiomatic features in C++ you may wish to use over the malloc
or even the global ::operator new
.
You should look to the smart pointer std::unique_ptr<>
or std::shared_ptr<>
and allow them to take care of the memory management issues.
Solution 2:
Depending on the definition of Data
, your code might be broken. It's bad code, either way.
If Data
is a plain old data type (POD, i.e. a typedef for a basic type, a struct of POD types etc.), and the allocated memory is properly aligned for the type (*), then your code is well-defined, which means it will "work" (as long as you initialize each member of *data_d
before using it), but it is not good practice. (See below.)
If Data
is a non-POD type, you are heading for trouble: The pointer assignment would not have invoked any constructors, for example. data_d
, which is of type "pointer to Data
", would effectively be lying because it points at something, but that something is not of type Data
because no such type has been created / constructed / initialized. Undefined behaviour will be not far off at that point.
The solution for properly constructing an object at a given memory location is called placement new:
Data * data_d = new (data) Data();
This instructs the compiler to construct a Data
object at the location data
. This will work for POD and non-POD types alike. You will also need to call the destructor (data_d->~Data()
) to make sure it is run before delete
ing the memory.
Take good care to never mix the allocation / release functions. Whatever you
malloc()
needs to befree()
d, what is allocated withnew
needsdelete
, and if younew []
you have todelete []
. Any other combination is UB.
In any case, using "naked" pointers for memory ownership is discouraged in C++. You should either
put
new
in a constructor and the correspondingdelete
in the destructor of a class, making the object the owner of the memory (including proper deallocation when the object goes out of scope, e.g. in the case of an exception); oruse a smart pointer which effectively does the above for you.
(*): Implementations are known to define "extended" types, the alignment requirements of which are not taken into account by malloc(). I'm not sure if language lawyers would still call them "POD", actually. MSVC, for example, does 8-byte alignment on malloc() but defines the SSE extended type __m128
as having a 16-byte alignment requirement.
Solution 3:
The rules surrounding strict aliasing can be quite tricky.
An example of strict aliasing is:
int a = 0;
float* f = reinterpret_cast<float*>(&a);
f = 0.3;
printf("%d", a);
This is a strict aliasing violation because:
- the lifetime of the variables (and their use) overlap
- they are interpreting the same piece of memory through two different "lenses"
If you are not doing both at the same time, then your code does not violate strict aliasing.
In C++, the lifetime of an object starts when the constructor ends and stops when the destructor starts.
In the case of built-in types (no destructor) or PODs (trivial destructor), the rule is instead that their lifetime ends whenever the memory is either overwritten or freed.
Note: this is specifically to support writing memory managers; after all malloc
is written in C and operator new
is written in C++ and they are explicitly allowed to pool memory.
I specifically used lenses instead of types because the rule is a bit more difficult.
C++ generally use nominal typing: if two types have a different name, they are different. If you access a value of dynamic type T
as if it were a U
, then you are violating aliasing.
There are a number of exceptions to this rule:
- access by base class
- in PODs, access as a pointer to the first attribute
And the most complicated rule is related to union
where C++ shifts to structural typing: you can access a piece of memory through two different types, if you only access parts at the beginning of this piece of memory in which the two types share a common initial sequence.
§9.2/18 If a standard-layout union contains two or more standard-layout structs that share a common initial sequence, and if the standard-layout union object currently contains one of these standard-layout structs, it is permitted to inspect the common initial part of any of them. Two standard-layout structs share a common initial sequence if corresponding members have layout-compatible types and either neither member is a bit-field or both are bit-fields with the same width for a sequence of one or more initial members.
Given:
struct A { int a; };
struct B: A { char c; double d; };
struct C { int a; char c; char* z; };
Within a union X { B b; C c; };
you can access x.b.a
, x.b.c
and x.c.a
, x.c.c
at the same time; however accessing x.b.d
(respectively x.c.z
) is a violation of aliasing if the currently stored type is not B
(respectively not C
).
Note: informally, structural typing is like mapping down the type to a tuple of its fields (flattening them).
Note: char*
is specifically exempt from this rule, you can view any piece of memory through char*
.
In your case, without the definition of Data
I cannot say whether the "lenses" rule could be violated, however since you are:
- overwriting memory with
Data
before accessing it throughData*
- not accessing it through
int*
afterwards
then you are compliant with the lifetime rule, and thus there is no aliasing taking place as far as the language is concerned.
Solution 4:
As long as the memory is used for only one thing at a time it's safe. You're basically use the allocated data as a union
.
If you want to use the memory for instances of classes and not only simple C-style structures or data-types, you have to remember to do placement new to "allocate" the objects, as this will actually call the constructor of the object. The destructor you have to call explicitly when you're done with the object, you can't delete
it.
Solution 5:
As long as you only handle "C"-types, this would be ok. But as soon as you use C++ classes you will get into trouble with proper initialization. If we assume that Data
would be std::string
for example, the code would be very wrong.
The compiler cannot really move the store across the call to printf
, because that is a visible side effect. The result has to be as if the side effects are produced in the order the program prescribes.