Aligned_Storage and Strict Aliasing

aligned_storage and strict aliasing

ABICT your use is safe.

  • Placement new of an object of type T will create an object starting at the address passed in.

§5.3.4/10 says:

A new-expression passes the amount of space requested to the
allocation function as the first argument of type std::size_t. That
argument shall be no less than the size of the object being created;
it may be greater than the size of the object being created only if
the object is an array.

For a non-array object, the size allocated cannot be greater that the size of the object, so the object representation must start at the beginning of the allocated memory in order to fit.

Placement new returns the pointer passed in (see § 18.6.1.3/2) as the result of the "allocation", so the object representation of the constructed object will start at that address.

  • static_cast<> and implicit conversions between T* type and void* convert between a pointer to the object and a pointer to its storage, if the object is a complete object.

§4.10/2 says:

A prvalue of type “pointer to cv T,” where T is an object type, can be
converted to a prvalue of type “pointer to cv void”. The result of
converting a “pointer to cv T” to a “pointer to cv void” points to the
start of the storage location where the object of type T resides, as
if the object is a most derived object (1.8) of type T [...]

This defines the implicit conversion to convert as stated. Further §5.2.9[expr.static.cast]/4 defines static_cast<> for explicit conversions, where an implicit conversion exists to have the same effect as the implicit conversion:

Otherwise, an expression e can be explicitly converted to a type T
using a static_cast of the form static_cast<T>(e) if the declaration
T t(e); is well-formed, for some invented temporary variable t (8.5).
The effect of such an explicit conversion is the same as performing
the declaration and initialization and then using the temporary
variable as the result of the conversion. [...]

For the inverse static_cast<> (from void* to T*), §5.2.9/13 states:

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. [...]
A value of type pointer to object converted to “pointer to cv void”
and back, possibly with different cv-qualification, shall have its
original value.

So if you have a void* pointing to the storage of the T object (which is the pointer value that would result from the implicit conversion of a T* to the object, then a static_cast of that to a T* will produce a valid pointer to the object.

Returning to your question, the preceding points imply that if you have

typename std::aligned_storage<sizeof(T), std::alignment_of<T>::value>::type t_;
void * pvt_ = &t_;

T* pT = new (&t_) T(args...);
void * pvT = pT;

then

  • the storage of *pT exactly overlays the first size(T) bytes of the storage of t_, so that pvT == pvt_
  • pvt_ == static_cast<void*>(&t_)
  • static_cast<T*>(pvT) == pT
  • Taken together that yields static_cast<T*>(static_cast<void*>(&t_)) == pT

How to avoid strict aliasing errors when using aligned_storage

std::aligned_storage is part of <type_traits>; like most of the rest of the inhabitants of that header file, it is just a holder for some typedefs and is not meant to be used as a datatype. Its job is to take a size and alignment, and make you a POD type with those characteristics.

You cannot use std::aligned_storage<Len, Align> directly. You must use std::aligned_storage<Len, Align>::type, the transformed type, which is "a POD type suitable for for use as uninitialized storage for any object whose size is at most Len and whose alignment is a divisor of Align." (Align defaults to the largest useful alignment greater than or equal to Len.)

As the C++ standard notes, normally the type returned by std::aligned_storage will be an array (of the specified size) of unsigned char with an alignment specifier. That avoids the "no strict aliasing" rule because a character type may alias any other type.

So you might do something like:

template<typename T>
using raw_memory = typename std::aligned_storage<sizeof(T),
std::alignment_of<T>::value>::type;

template<typename T>
void* allocate() { return static_cast<void*>(new raw_memory<T>); }

template<typename T, typename ...Arg>
T* maker(Arg&&...arg) {
return new(allocate<T>()) T(std::forward<Arg>(arg)...);
}

Does reinterpret_casting std::aligned_storage* to T* without std::launder violate strict-aliasing rules?

I asked a related question in the ISO C++ Standard - Discussion forum. I learned the answer from those discussions, and write it here to hope to help someone else who is confused about this question. I will keep updating this answer according to those discussions.

Before P0137, refer to [basic.compound] paragraph 3:

If an object of type T is located at an address A, a pointer of type cv T* whose value is the address A is said to point to that object, regardless of how the value was obtained.

and [expr.static.cast] paragraph 13:

If the original pointer value represents the address A of a byte in memory and A satisfies the alignment requirement of T, then the resulting pointer value represents the same address as the original pointer value, that is, A.

The expression reinterpret_cast<const T*>(data+pos) represents the address of the previously created object of type T, thus points to that object. Indirection through this pointer indeed get that object, which is well-defined.

However after P0137, the definition for a pointer value is changed and the first block-quoted words is deleted. Now refer to [basic.compound] paragraph 3:

Every value of pointer type is one of the following:

  • a pointer to an object or function (the pointer is said to point to the object or function), or

  • ...

and [expr.static.cast] paragraph 13:

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.

The expression reinterpret_cast<const T*>(data+pos) still points to the object of type std::aligned_storage<...>::type, and indirection get a lvalue referring to that object, though the type of the lvalue is const T. Evaluation of the expression v1[0] in the example tries to access the value of the std::aligned_storage<...>::type object through the lvalue, which is undefined behavior according to [basic.lval] paragraph 11 (i.e. the strict-aliasing rules):

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:

  • the dynamic type of the object,

  • a cv-qualified version of the dynamic type of the object,

  • a type similar (as defined in [conv.qual]) to the dynamic type of the object,

  • a type that is the signed or unsigned type corresponding to the dynamic type of the object,

  • a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,

  • an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),

  • a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,

  • a char, unsigned char, or std​::​byte type.

Small object stack storage, strict-aliasing rule and Undefined Behavior

First, use std::aligned_storage_t. That is what it is meant for.

Second, the exact size and layout of virtual types and their decendants is compiler-determined. Allocating a derived class in a block of memory then converting the address of that block to a base type may work, but there is no guarantee in the standard it will work.

In particular, if we have struct A {}; struct B:A{}; there is no guarantee unless you are standard layout that a pointer-to-B can be reintepreted as a pointer-to-A (especially throught a void*). And classes with virtuals in them are not standard layout.

So the reinterpretation is undefined behavior.

We can get around this.

struct func_vtable {
void(*invoke)(void*) = nullptr;
void(*destroy)(void*) = nullptr;
};
template<class T>
func_vtable make_func_vtable() {
return {
[](void* ptr){ (*static_cast<T*>(ptr))();}, // invoke
[](void* ptr){ static_cast<T*>(ptr)->~T();} // destroy
};
}
template<class T>
func_vtable const* get_func_vtable() {
static const auto vtable = make_func_vtable<T>();
return &vtable;
}

class Func{
func_vtable const* vtable = nullptr;
std::aligned_storage_t< 64 - sizeof(func_vtable const*), sizeof(void*) > data;

public:
Func() = delete;
Func(const Func&) = delete;

template<class F, class dF=std::decay_t<F>>
Func(F&& f){
static_assert(sizeof(dF) <= sizeof(data), "");
new(static_cast<void*>(&data)) dF(std::forward<F>(f));
vtable = get_func_vtable<dF>();
}

void operator () (){
return vtable->invoke(&data);
}

~Func(){
if(vtable) vtable->destroy(&data);
}
};

This no longer relies upon pointer conversion guarantees. It simply requires that void_ptr == new( void_ptr ) T(blah).

If you are really worried about strict aliasing, store the return value of the new expression as a void*, and pass that into invoke and destroy instead of &data. That is going to be beyond reproach: the pointer returned from new is the pointer to the newly constructed object. Access of the data whose lifetime has ended is probably invalid, but it was invalid before as well.

When objects begin to exist and when they end is relatively fuzzy in the standard. The latest attempt I have seen to solve this issue is P0137-R1, where it introduces T* std::launder(T*) to make the aliasing issues go away in an extremely clear manner.

The storage of the pointer returned by new is the only way I know of that clearly and unambiguously does not run into any object aliasing problems prior to P0137.

The standard did state:

If an object of type T is located at an address A, a pointer of type cv T* whose value is the address A is said to point to that object, regardless of how the value was obtained

the question is "does the new expression actually guarantee that the object is created at the location in question". I was unable to convince myself it states so unambiguously. However, in my own type erasure implementions, I do not store that pointer.

Practically, the above is going to do much the same as many C++ implementations do with virtual functions tables in simple cases like this, except there is no RTTI created.

Memory alignment and strict aliasing for continuous block of raw bytes

First, it is unclear from your question, but I will assume that there is no other code inbetween the individual snippets you are showing.

Snippet 1. has undefined behavior because the pointer get will return cannot actually be pointing to a float object. ::operator new does implicitly create objects and return a pointer to a suitable created object, but that object would have to be a unsigned char object part of an unsigned char array in order to give the pointer arithmetic in reinterpret_cast<unsigned char*>(data) + bshift defined behavior.

However, then the return value of get<float>(sizeof(float)); would also be a pointer to an unsigned char object. Writing through a float glvalue to a unsigned char violates the aliasing rules.

This could be remedied by either using std::launder before returning the pointer from get or better by explicitly creating the object:

template<class T>
T* get(std::size_t bshift)
{
return new(reinterpret_cast<unsigned char*>(data) + bshift) T;
}

Although this will create a new object with indeterminate value each time it is called.

std::launder would be sufficient here without creating a new object since ::operator new can implicitly create an unsigned char array which provides storage for a float object which is also implicitly-created. (Assuming all objects used in this way fit in the storage, are correctly aligned (see below), do not overlap and are implicit-lifetime types):

template<class T>
T* get(std::size_t bshift)
{
return std::launder(reinterpret_cast<T*>(reinterpret_cast<unsigned char*>(data) + bshift));
}

However, 2. and 3. have undefined behavior even with this modification if alignof(float) != 1 (which is very likely to be true). You cannot create and start the lifetime of an object with wrong alignment either implicitly or explicitly. (Although it may technically be possible to create an object with wrong alignment explicitly without starting its lifetime.)

For 4. and 5., assuming the above weren't undefined behavior due to misalignment or out-of-bounds access and given the assumptions in the question, I think these snippets should have defined behavior. Note however that it is extremely unlikely that these requirements are satisfied.

For 6., if you offset everything you need to again take care not to go out-of-bounds of the allocation and not to violate the alignment of any of the involved types. (Including the alignment of POD_struct for 5.)

For 7. the formulation is very vague, so I am not sure what you mean. But you explicitly generally can't interpret memory as a different type than it is. In your examples 4. and 5. you are copying object representations, which is different.


To be clear again: Practically speaking your code has UB due to the alignment violations. This probably extends even to platforms that allow unaligned access, because the compiler may optimize code based on the assumption of pointer alignment. Compilers may offer type annotations to indicate that a pointer may be unaligned. You need to use these or other tools if you want to implement unaligned access in practice.

What is the purpose of std::aligned_storage?

You can use std::aligned_storage whenever you wish to decouple memory allocation from object creation.

You claim:

Also it is usable only with POD types.

But this is not true. There is nothing preventing std::aligned_storage from being used with non-POD types.

The example on cppreference provides a legitimate use case:

template<class T, std::size_t N>
class static_vector
{
// properly aligned uninitialized storage for N T's
typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
std::size_t m_size = 0;
...

The idea here is that once the static_vector is constructed, memory is immediately allocated for N objects of type T, but no objects of type T are created yet.

You cannot do that with a simple T data[N]; array member, because this would immediately run T's constructor for each element, or wouldn't even compile if T is not default-constructible.



Related Topics



Leave a reply



Submit