0

Is the following well defined behavior?

#include <cstdlib>
#include <iostream>

void reallocate_something(void *&source_and_result, size_t size) {
    void *dest = malloc(size);
    memcpy(dest, source_and_result, size);
    free(source_and_result);
    source_and_result = dest;
}

void reallocate_something(int *&source_and_result, size_t size) {
    // I the cast safe in this use case?
    reallocate_something(reinterpret_cast<void*&>(source_and_result), size);
}

int main() {
    const size_t size = 4 * sizeof(int);
    int *start = static_cast<int*>(malloc(size));
    *start = 0;

    std::cout << start << ' ' << *start << '\n';
    reallocate_something(start, size);
    std::cout << start << ' ' << *start << '\n';

    return 0;
}
  

The code uses a reinterpret_cast to pass a reference to a pointer and re-allocate it, free it, and set it to the new area allocated. Is this well defined?

In particular A static_cast would work if I did not want to pass a reference, and that would be well defined.

The tag is C++, and I'm asking about this code as-is within the C++ standard.

kabanus
  • 24,623
  • 6
  • 41
  • 74
  • This is not well defined for `void*` and `int*` are not similar. Refer to **Type aliasing** section [here](https://en.cppreference.com/w/cpp/language/reinterpret_cast). – Lingxi Oct 20 '21 at 05:56
  • @Lingxi Isn't that to simplified? `malloc` always takes and returns `void*`, and yet we allocate any pointer with that (and thus have to cast). – kabanus Oct 20 '21 at 06:31
  • Why not `std::realloc`? – Andrej Podzimek Oct 20 '21 at 06:33
  • @AndrejPodzimek Because I'm checking an issue of understanding the standard and undefined behavior, not best practices. You might as well say why not use `std::array` for this example. – kabanus Oct 20 '21 at 06:35
  • 1
    is `&` in `reinterpret_cast(source_and_result)` necessary in this function call `reallocate_something(reinterpret_cast(source_and_result), size);`? – Harry Oct 20 '21 at 06:48
  • @Harry The functions takes an original pointer, copies at somewhere else, frees the original pointer and changes the original pointer passed (thus why I want a reference in this example). I thought I could use a static cast somehow, or perhaps forwarding , but none of that worked. If this is undefined I'll use a `**` and pass the address of the original pointer rather than a reference (in the inner call to the `void*` taking function. If I did not need a reference a normal static cast would work, but I'd have to return the new pointer (different design). – kabanus Oct 20 '21 at 06:51
  • @kabanus You missed the point. Anyway, I posted an answer. – Lingxi Oct 20 '21 at 08:56
  • The first occurrence of `*start` causes undefined behaviour by reading uninitialized memory – M.M Oct 20 '21 at 09:54
  • @M.M I don't think that's true in the strict sense. UB (in the language lawyer sense) does not mean unpredictable in a general sense. I think it's guaranteed by the standard that whatever was sitting in the allocated memory would be printed, whatever that is. As long as `malloc` was called (or whatever storage allocation you used), I think it's not UB. Anyway, the second print is the same. – kabanus Oct 20 '21 at 09:58
  • @kabanus there is no guarantee in the standard of "whatever is sitting in the allocated memory" -- the behaviour of reading such memory is undefined "in the language lawyer sense" (which is the only sense of UB since the standard defines the meaning of the term) – M.M Oct 20 '21 at 21:43
  • @M.M Thanks, I thought it was merely unspecified in this case. I corrected the example. – kabanus Oct 21 '21 at 03:34

4 Answers4

2

Is the following well defined behavior?

No, it's not. You can't interpret int * pointer with void * handle, int and void are not similar types. You can convert an int * pointer to void * and back. If your function takes a reference, to do the conversion you need a new temporary variable of type void * to hold the result of the conversion, and then you have to assign it back, like in the other answer https://stackoverflow.com/a/69641609/9072753 .

Anyway, just make it a template, and write nice C++ code with placement new. Something along:

template<typename T>
void reallocate_something(T *&pnt, size_t cnt) {
    T *dest = reinterpret_cast<T *>(malloc(cnt * sizeof(T)));
    if (dest == NULL) throw ...;
    for (size_t i = 0; i < cnt; ++i) {
       new (dest[i]) T(pnt[i]);
    }
    free(static_cast<void*>(pnt));
    pnt = dest;
}
KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • doesn't this example assume that pointers can be strictly aligned back to back? – Thomas Oatman Nov 02 '22 at 21:08
  • I do not understand what "strictly aligned" and "aligned back to back" means. `malloc` returns a pointer properly aligned for any object. – KamilCuk Nov 02 '22 at 21:21
  • @KamiCuk :-) malloc made a contiguous run of ram. Which is then accessed as an array. But if the size of the object in the array is less than the machine’s alignment, won’t it need more than simply sizeof(obj) x num ? I’ve only worked on Intel platform, so never had to worry with it myself. – Thomas Oatman Nov 06 '22 at 04:59
  • `if the size of the object in the array is less than the machine’s alignment` For all standard types and objects that have to be false. If you make a custom type with compiler extensions, that's on you that you can't use malloc. I found https://stackoverflow.com/questions/46457449/is-it-always-the-case-that-sizeoft-alignoft-for-all-object-types-t – KamilCuk Nov 06 '22 at 09:13
  • Thanks a lot @KamiCuk. I did a lot of playing in Visual Studio with arrays, structs, sizeof(), alignof() on 64bit compile . . . I still have to be curious what happens on hardware that will cause exceptions if you access unaligned memory. So byte a[10]; on a 4 or 8-byte hardware: Is a[1] one byte after a[0] ? If it is, wouldn't you get an exception accessing a[2], a[2], etc If not, that seems like sizeof(byte) on that hardware would be the hardware alignment..... so sizeof(byte [10]) would be 40 on a 4-byte aligned machine.... instead of 10? – Thomas Oatman Nov 07 '22 at 16:29
  • `what happens on hardware that will cause exceptions if you access unaligned memory` It depends on the specific hardware and specific instruction. https://stackoverflow.com/questions/12491578/whats-the-actual-effect-of-successful-unaligned-accesses-on-x86 `So byte a[10]; on a 4 or 8-byte hardware` I do not understand the example, a byte is a byte, 4 bytes is a word. The compiler generates code so that bytes are accessed with instructions for bytes, so all is aligned properly. https://stackoverflow.com/q/58628157/9072753 – KamilCuk Nov 07 '22 at 16:41
1

Actually I'm not sure but I feel this is the correct way to do.

#include <iostream>
#include <cstring>

void reallocate_something(void *&source_and_result, size_t size) {
    void *dest = malloc(size);
    memcpy(dest, source_and_result, size);
    free(source_and_result);
    source_and_result = dest;
}

void reallocate_something(int *&source_and_result, size_t size) {
    // Is the cast safe in this use case?
    void *temp = static_cast<void*>(source_and_result);
    reallocate_something(temp, size);
    source_and_result = static_cast<int*>(temp);
}

int main() {
    const size_t size = 4 * sizeof(int);
    int *start = static_cast<int*>(malloc(size));

    std::cout << start << ' ' << *start << '\n';
    reallocate_something(start, size);
    std::cout << start << ' ' << *start << '\n';

    return 0;
}
kabanus
  • 24,623
  • 6
  • 41
  • 74
Harry
  • 2,177
  • 1
  • 19
  • 33
  • 1
    Not bad. Note you can use a `static_cast` which is safer, and shows the value of your answer. The reason I prefer it "inside" (the re-assignment) is that I will have multiple overloads though. – kabanus Oct 20 '21 at 07:02
1

This is not well defined for void* and int* are not similar. Refer to Type aliasing section here.

Note that pointer round trip via void* like below is well defined. Particularly, there is no type aliasing here.

T* pt = ...;

void* p = pt;

auto pt2 = static_cast<T*>(p);
assert(pt2 == pt);

This is different from following code with type aliasing which is not well defined.

T* pt = ...;

void* p = nullptr;
reinterpret_cast<T*&>(p) = pt; // or *reinterpret_cast<T**>(&p) = pt;

auto pt2 = static_cast<T*>(p);
assert(pt2 == pt);

It follows that your sample code can be revised as below.

void reallocate_something(int *&source_and_result, size_t size) {
    void* p = source_and_result;
    reallocate_something(p, size);
    source_and_result = static_cast<int*>(p);
}

Or better yet

void* reallocate_something(void *source_and_result, size_t size) {
    void *dest = malloc(size);
    memcpy(dest, source_and_result, size);
    free(source_and_result);
    return dest;
}

void reallocate_something(int *&source_and_result, size_t size) {
    source_and_result = static_cast<int*>(reallocate_something(source_and_result, size));
}
Lingxi
  • 14,579
  • 2
  • 37
  • 93
  • Thanks, I this is what Harry had as well. I wanted the assignment "inside" on purpose in my example, but this is a valid solution. – kabanus Oct 20 '21 at 09:46
  • @kabanus Added another one which I think is better. BTW, what's your original goal/purpose/motivation? – Lingxi Oct 20 '21 at 10:45
  • Yea, that solution was also something I was considering once I was sure the type alias was undefined. The real purpose is refactoring a lot of Cuda gpu allocations, and re-purposing the original pointers to point to these. "malloc" was an alias for the Cuda operations. With the solutions here the whole data transfer API and preparing the data structures becomes considerably shorter and nicer (easier to read). – kabanus Oct 20 '21 at 11:04
0

There exist platforms where the bitwise representations of int* and void* are incompatible. On such platforms, it would be often be impossible for a compiler to allow a reference of one type to meaningfully act upon an object of the other, and the Standard thus refrains from requiring that implementations do so.

Of course, the vast majority of platforms use the same representation for all PODS pointer types, and when the Standard was written it was obvious to pretty much everyone that (1) it allowed compilers for such platforms to process reference type casts usefully, and (2) compilers should process such casts usefully except when there was a compelling reason to do otherwise. It was expected that the only compiler writers who would care about whether such conversions had defined behavior would be those targeting platforms where such support would be expensive (e.g. requiring that any int* whose address is taken be stored using the same bit pattern as void*, even if that would require shuffling its bits around when using it to fetch an int), and compiler writers were expected know more about the costs and benefits of such support than the Committee ever could.

Most implementations can be configured to process such casts in the manner that would have been expected when the Standard was written, but the Standard does not mandate such support; when such configurations, the behavior of the construct should be regarded as defined by a popular language extension.

supercat
  • 77,689
  • 9
  • 166
  • 211