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 betweenT*
type andvoid*
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 typeT
using astatic_cast
of the formstatic_cast<T>(e)
if the declaration
T t(e);
is well-formed, for some invented temporary variablet
(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 oft_
, so thatpvT == 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 reintepret
ed as a pointer-to-A
(especially throught a void*
). And classes with virtual
s 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
Reading and Writing to Usb (Hid) Interrupt Endpoints on MAC
How to Pan Images in Qgraphicsview
Is Std::String Thead-Safe with Gcc 4.3
What Does 'Value Initializing' Something Mean
C++11 Constexpr Flatten List of Std::Array into Array
Portability of Native C++ Properties
Getting Useful Gcov Results for Header-Only Libraries
What Are Differences Between Std, Tr1 and Boost (As Namespaces And/Or Libraries)
How to Build Libcxx and Libcxxabi by Clang on Centos 7
C++ Object Instantiation VS Assignment
What Strategies Have You Used to Improve Build Times on Large Projects
How to Speed Up Floating-Point to Integer Number Conversion
Std::Make_Tuple Doesn't Make References
What Could Go Wrong If Copy-List-Initialization Allowed Explicit Constructors