8

I'm still struggling to understand what's allowed and not allowed with strict aliasing. With this concrete example is it violation of strict aliasing rule? If not, why? Is it because I placement new a different type into a char* buffer?

template <typename T>
struct Foo
{
    struct ControlBlock { unsigned long long numReferences; };
    Foo()
    {
        char* buffer = new char[sizeof(T) + sizeof(ControlBlock)];
        // Construct control block
        new (buffer) ControlBlock{};
        // Construct the T after the control block
        this->ptr = buffer + sizeof(ControlBlock);
        new (this->ptr) T{};
    }
    char* ptr;

    T* get() { 
        // Here I cast the char* to T*.
        // Is this OK because T* can alias char* or because
        // I placement newed a T at char*
        return (T*)ptr;
    }
};

For the record, a void* can alias any other type pointer, and any type pointer can alias a void*. A char* can alias any type pointer, but is the reverse true? Can any type alias a char* assuming the alignment is correct? So is the following allowed?

char* buffer = (char*)malloc(16);
float* pFloat = buffer;
*pFloat = 6; // Can any type pointer alias a char pointer?
// If the above is illegal, then how about:
new (pFloat) float; // Placement new construct a float at pointer
*pFloat = 7; // What about now?

Once I've assigned char* buffer pointer to the new allocation, in order to use it as a float buffer do I need to loop through and placement new a float at each place? If I had not assigned the allocation to a char* in the first place, but a float* to begin with, I'd be able to use it immediately as a float buffer, right?

Zebrafish
  • 11,682
  • 3
  • 43
  • 119

2 Answers2

6

Is this strict aliasing violation?

Yes.

Can any type pointer alias a char pointer?

No.

You can launder the pointer:

T* get() { 
    return std::launder(reinterpret_cast<T*>(ptr)); // OK
}

Or, you could store the result of the placement new:

Foo()
{
    ...
    this->ptr = new (buffer + sizeof(ControlBlock)) T{};
}
T* ptr;

T* get() { 
    return ptr; // OK
}

do I need to loop through and placement new a float at each place

Not since the proposal P0593R6 was accepted into the language (C++20). Prior to that, placement-new was required by the standard. You don't necessarily have to write that loop yourself since there are function templates for that in the standard library: std::uninitialized_fill_n, uninitialized_default_construct_n etc. Also, you can rest assured that a decent optimiser will compile such loop to zero instructions.

constexpr std::size_t N = 4;
float* pFloat = static_cast<float*>(malloc(N * sizeof(float)));

// OK since P0593R6, C++20
pFloat[0] = 6;

// OK prior to P0593R6, C++20 (to the extent it can be OK)
std::uninitialized_default_construct_n(pFloat, N);
pFloat[0] = 7;

// don't forget
free(pFloat);

P.S. Don't use std::malloc in C++, unless you need it for interacting with C API that requires it (which is a somewhat rare requirement even in C). I also recommend against reusal of new char[] buffer as it is unnecessary for the demonstrated purpose. Instead, use the operator ::new which allocates storage without creating objects (even trivial ones). Or even better, since you already have a template, let the user of the template provide an allocator of their own to make your template more generally useful.

eerorika
  • 232,697
  • 12
  • 197
  • 326
6

Strict aliasing means that to dereference a T* ptr, there must be a T object at that address, alive obviously. Effectively this means you cannot naively bit-cast between two incompatible types and also that a compiler can assume that no two pointers of incompatible types point to the same location.

The exception is unsigned char , char and std::byte, meaning you can reinterpret cast any object pointer to a pointer of these 3 types and dereference it.

(T*)ptr; is valid because at ptr there exists a T object. That is all that is required, it does not matter how you got that pointer*, through how many casts it went. There are some more requirements when T has constant members but that has to do more with placement new and object resurrection - see this answer if you are interested.

*It does matter even in case of no const members, probably, not sure, relevant question . @eerorika 's answer is more correct to suggest std::launder or assigning from the placement new expression.

For the record, a void* can alias any other type pointer, and any type pointer can alias a void*.

That is not true, void is not one of the three allowed types. But I assume you are just misinterpreting the word "alias" - strict aliasing only applies when a pointer is dereferenced, you are of course free to have as many pointers pointing to wherever you want as long as you do not dereference them. Since void* cannot be dereferenced, it's a moo point.

Addresing your second example

char* buffer = (char*)malloc(16); //OK

// Assigning pointers is always defined the rules only say when
// it is safe to dereference such pointer.
// You are missing a cast here, pointer cannot be casted implicitly in C++, C produces a warning only.
float* pFloat = buffer; 
// -> float* pFloat =reinterpret_cast<float*>(buffer);

// NOT OK, there is no float at `buffer` - violates strict aliasing.
*pFloat = 6;
// Now there is a float
new (pFloat) float;
// Yes, now it is OK.
*pFloat = 7;
Quimby
  • 17,735
  • 4
  • 35
  • 55
  • I'm really having trouble understanding this. So an object as to exist at that memory location. However float* pToFloat = (float*)malloc(16); no float exists at that memory yet I can use this pointer with no problems. Is assigning the float* to the allocation only problematic if I assign another char* to it? – Zebrafish Aug 20 '21 at 13:50
  • @Zebrafish Addressed the original example. No, you cannot use it, it is undefined behaviour in C++. Intermediary casts cannot break or fix the code. – Quimby Aug 20 '21 at 13:53
  • See [this answer](https://stackoverflow.com/a/49299817/7691729) for why `malloc` is valid in C. – Quimby Aug 20 '21 at 13:55
  • So if it's about what object exists there only, then I can MyStruct1* p1 = malloc(16); MyStruct2* p2 = (MyStruct2*) p1; new (p2) MyStruct2{ }; Then access p2. That answer you linked says that the memory returned by malloc has no type in its memory, so we alias that memory with any pointer. The memory is of type of whatever you write into it, so if you write a float into the buffer returned by malloc, then you can't alias it with another type. So if I go char* pBuffer = (char*)malloc(16); why can't I go float* pFloat = (float*)pBuffer; and use it if there's no type in that buffer yet? – Zebrafish Aug 20 '21 at 14:11
  • What you said is correct for C, not C++ - malloc itself does not create objects, you need (placement) new for that. – Quimby Aug 20 '21 at 14:12
  • 1
    @Zebrafish , [it **is** a moo point](https://www.youtube.com/watch?v=fLwYpSCrlHU) :) – Quimby Aug 20 '21 at 14:17
  • @Zebrafish I may have been wrong about the examples, `std::launder` or at least assignment from the placement new. There are some complicated rules about how `ptr` might point to an "old" address. See @eerorika 's answer and accept it you want as I think it is better in this regard. – Quimby Aug 20 '21 at 14:23
  • It seems strange that I can do MyStruct1* p1 = malloc(16); MyStruct2* p2 = (MyStruct2*) p1; new (p2) MyStruct2{ }; Then access p2. Are you sure about this? Although this follows the rule that you can access the memory because the right object has been constructed there, it's still aliasing with a different pointer type. – Zebrafish Aug 20 '21 at 14:48
  • @Zebrafish What you are doing is not aliasing, aliasing must involve a dereference. you cannot dereference `p1` and neither `p2` before the placement new. Also, as eerorika rightfully stated, `p2` should be reassigned from the placement new like `p2= new (p2) MyStruct2()`. You can have as many different pointers to the same address as you like, I mean what about `int* p1=0;float* p2=0; T* p3=0;` surely that is not breaking any rules, right? – Quimby Aug 20 '21 at 19:44
  • And the difference between p2= new (p2) MyStruct2(); and just new (p2) MyStruct2();? That assign is needed why? If placement new constructs at pointed location of p2? – Zebrafish Aug 20 '21 at 23:55
  • @Zebrafish That's rather complicated and I am not 100% confident, https://stackoverflow.com/a/27049038/7691729 asks essentially the same. – Quimby Aug 21 '21 at 09:52