Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

The following example comes from std::aligned_storage page of cppreference.com:

#include <iostream>
#include <type_traits>
#include <string>

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;

public:
    // Create an object in aligned storage
    template<typename ...Args> void emplace_back(Args&&... args) 
    {
        if( m_size >= N ) // possible error handling
            throw std::bad_alloc{};
        new(data+m_size) T(std::forward<Args>(args)...);
        ++m_size;
    }

    // Access an object in aligned storage
    const T& operator[](std::size_t pos) const 
    {
        return *reinterpret_cast<const T*>(data+pos);
    }

    // Delete objects from aligned storage
    ~static_vector() 
    {
        for(std::size_t pos = 0; pos < m_size; ++pos) {
            reinterpret_cast<T*>(data+pos)->~T();
        }
    }
};

int main()
{
    static_vector<std::string, 10> v1;
    v1.emplace_back(5, '*');
    v1.emplace_back(10, '*');
    std::cout << v1[0] << '\n' << v1[1] << '\n';
}

In the example, the operator[] just reinterpret_casts std::aligned_storage* to T* without std:launder, and performs an indirection directly. However, according to this question, this seems to be undefined, even if an object of type T has been ever created.

So my question is: does the example program really violate strict-aliasing rules? If it does not, what's wrong with my comprehension?

like image 650
xskxzr Avatar asked Dec 10 '17 03:12

xskxzr


People also ask

What is the purpose of std :: launder?

Typical uses of std::launder include: Obtaining a pointer to an object created in the storage of an existing object of the same type, where pointers to the old object cannot be reused (for instance, because either object is a base class subobject);

What is reinterpret_ cast in c++?

reinterpret_cast is a type of casting operator used in C++. It is used to convert a pointer of some data type into a pointer of another data type, even if the data types before and after conversion are different. It does not check if the pointer type and data pointed by the pointer is same or not.


2 Answers

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.

like image 146
xskxzr Avatar answered Sep 27 '22 21:09

xskxzr


The code doesn't violate the strict aliasing rule in any way. An lvalue of type const T is used to access an object of type T, which is permitted.

The rule in question, as covered by the linked question, is a lifetime rule; C++14 (N4140) [basic.life]/7. The problem is that, according to this rule, the pointer data+pos may not be used to manipulate the object created by placement-new. You're supposed to use the value "returned" by placement-new.

The question naturally follows: what about the pointer reinterpret_cast<T *>(data+pos) ? It is unclear whether accessing the new object via this new pointer violates [basic.life]/7.

The author of the answer you link to, assumes (with no justification offered) that this new pointer is still "a pointer that pointed to the original object". However it seems to me that it is also possible to argue that , being a T *, it cannot point to the original object, which is a std::aligned_storage and not a T.

This shows that the object model is underspecified. The proposal P0137, which was incorporated into C++17, was addressing a problem in a different part of the object model. But it introduced std::launder which is a sort of mjolnir to squash a wide range of aliasing, lifetime and provenance issues.

Undoubtedly the version with std::launder is correct in C++17. However, as far as I can see, P0137 and C++17 don't have any more to say about whether or not the version without launder is correct.

IMHO it is impractical to call the code UB in C++14, which did not have std::launder, because there is no way around the problem other than to waste memory storing all the result pointers of placement-new. If this is UB then it's impossible to implement std::vector in C++14, which is far from ideal.

like image 44
M.M Avatar answered Sep 27 '22 20:09

M.M