Why is std::aligned_storage to be deprecated in C++23 and what to use instead?
Asked Answered
C

1

67

I just saw that C++23 plans to deprecate both std::aligned_storage and std::aligned_storage_t as well as std::aligned_union and std::aligned_union_t.

Placement new'd objects in aligned storage are not particularly constexpr friendly as far as I understand, but that doesn't appear to be a good reason to throw out the type completely. This leads me to assume that there is some other fundamental problem with using std::aligned_storage and friends that I am not aware of. What would that be?

And is there a proposed alternative to these types?

Cholecystectomy answered 11/4, 2022 at 12:59 Comment(3)
The relevant proposal appears to be P1413R3 - Deprecate std::aligned_storage and std::aligned_union, which was indeed approved on Feb. 7 2022Balder
There's a note in the standard "Uses of aligned_­storage<Len, Align>​::​type can be replaced by an array std​::​byte[Len] declared with alignas(Align)."Elevenses
I wonder why they think that "Using aligned_* invokes undefined behavior". I've asked a question about this: Why does the use of std::aligned_storage allegedly cause UB due to it failing to "provide storage"?Pernod
R
67

Here are three excerpts from P1413R3:

Background

aligned_* are harmful to codebases and should not be used. At a high level:

  • Using aligned_* invokes undefined behavior (The types cannot provide storage.)
  • The guarantees are incorrect (The standard only requires that the type be at least as large as requested but does not put an upper bound on the size.)
  • The API is wrong for a plethora of reasons (See "On the API".)
  • Because the API is wrong, almost all usage involves the same repeated pre-work (See "Existing usage".)

On the API

std::aligned_* suffer from many poor API design decisions. Some of these are shared, and some are specific to each. As for what is shared, there are three main problems [only one is included here for brevity]:

  • Using reinterpret_cast is required to access the value

There is no .data() or even .data on std::aligned_* instances. Instead, the API requires you to take the address of the object, call reinterpret_cast<T*>(...) with it, and then finally indirect the resulting pointer giving you a T&. Not only does this mean that it cannot be used in constexpr, but at runtime it's much easier to accidentally invoke undefined behavior. reinterpret_cast being a requirement for use of an API is unacceptable.


Suggested replacement

The easiest replacement for aligned_* is actually not a library feature. Instead, users should use a properly-aligned array of std::byte, potentially with a call to std::max(std::initializer_list<T>) . These can be found in the <cstddef> and <algorithm> headers, respectively (with examples at the end of this section). Unfortunately, this replacement is not ideal. To access the value of aligned_*, users must call reinterpret_cast on the address to read the bytes as T instances. Using a byte array as a replacement does not avoid this problem. That said, it's important to recognize that continuing to use reinterpret_cast where it already exists is not nearly as bad as newly introducing it where it was previously not present. ...

The above section from the accepted proposal to retire aligned_* is then followed with a number of examples, like these two replacement suggestions:

// To replace std::aligned_storage
template <typename T>
class MyContainer {
private:
    //std::aligned_storage_t<sizeof(T), alignof(T)> t_buff;
    alignas(T) std::byte t_buff[sizeof(T)];
};
// To replace std::aligned_union
template <typename... Ts>
class MyContainer {
private:
    //std::aligned_union_t<0, Ts...> t_buff;
    alignas(Ts...) std::byte t_buff[std::max({sizeof(Ts)...})];
};
Riga answered 11/4, 2022 at 13:13 Comment(21)
Adding the "background" section improved the answer a lot, thanks.Surra
@FrançoisAndrieux I sort of agree with your previous comment. Many library features are quite possible to implement manually so that alone isn't a good reason to scrap a feature.Riga
The argument given by the proposal to remove the types are somewhat compelling, yes, but I don't get how using an aligned bye array is any different. Wouldn't that equally require reinterpret_casting?Cholecystectomy
@Cholecystectomy Yes, that's how I see it too. Both aligned_* and an aligned byte array require reinterpret_cast at some point. I added the section from the proposal regarding that which ends with "reinterpret_cast being a requirement for use of an API is unacceptable" - so I guess they had pretty strong feelings against it :-)Riga
Also, the argument that reinterpret_cast is a necessary part of ever using this idiom/type is false, because placement-new will return a proper T*. So a use-case that never requires reinterpreting any points is conceivable and plausible. Anyhow, it's gone now and people will have to roll their own.Cholecystectomy
@Cholecystectomy The proposal isn't particularly long or difficult, you can read it for the finer details.Elevenses
What we really need is a new way of declaring a properly typed array without constructing its elements yet, eg: [[uninitialized_storage]] T t_arr[len]; where the compiler would allocate the array of sufficient size and alignment, just not call any constructors on it. allowing the developer to use placement-new to initialize each T element as needed. I know, wishful thinking...Stallfeed
@RemyLebeau Nice idea! I'm not sure if I've walked into UB land with this uninitialized_array_t implementation, but I think it should work - or perhaps with unique_ptr support:Riga
@RemyLebeau Wouldn't such an attribute be effectively be syntactic sugar over just using a union and having T be the inactive member? In C++23 you could do something like union { T entry; } t_arr[len];. The union retains the size + alignment, and can even be used in constexpr as long as the object is only viewed if it's been initialized via std::construct_atFruitage
@Fruitage Wouldn't that default initialize the T:s in C++23 too? (I have very little knowledge about what's coming in C++23)Riga
@TedLyngmo If I understand the footnote on this answer by Barry, C++23 should be able to do this -- but I haven't been following c++23 that closely. The alternative that would definitely work would be to throw an empty trivial struct in the union to be active member when T is not (e.g. union { struct empty_type {} empty; T entry; } t_arr[len]. Then std::construct_at changes the active member legally, and entry can still be accessed in constexpr contexts without reinterpret_cast being needed.Fruitage
@TedLyngmo In order for class MyContainer to return a reference / pointer to T, how is this implemented without a reinterpret_cast on t_buff[0] ?Foofaraw
@Foofaraw Human-Compiler wrote that, not me, but std::construct_at is equivalent to return ::new (const_cast<void*>(static_cast<const volatile void*>(p))) T(std::forward<Args>(args)...); except that construct_at may be used in evaluation of constant expressions.Riga
Thanks @TedLyngmo, reading the Suggested replacement excerpt Using a byte array as a replacement does not avoid this problem, i.e. reinterpret_cast is still required to get a reference outside of the context of its construction.Foofaraw
This is retarded. std::bit_cast was invented specifically for these situations, and is usable within a constexpr context. Further, typedefs of aligned types are required in order to create allocator rebinds to that type, and pointer typedefs to that type.Chenab
@Chenab Are you addressing one of us (if so, who) or P1413R3?Riga
God I hate the C++ standard. They make everything so complicated and forget about the basics.Calisa
@TedLyngmo Your uninitialized_array_t uses a raw array as the storage. I changed it to std::array but still equivalent and interestingly, nothing went wrong and no warning was shown. The code compiled and ran ok. Do you think that it's really ok to do that? Or could it cause some faults?Ageold
@Ageold I think I followed the rules and I used alignas(T) std::byte buf[sizeof(T)]; as suggested P1413R3 for one element and only made it N times bigger. It'd require a language-lawyer to say for sure :-)Riga
@Calisa the purpose of the standard is to be comprehensive, targeting compiler implementers. That's not gonna be simple. It's the same difference you would find between the specification to manufacture your car, and the driver's manual.Lemons
@Lemons yeah, but they have fiddly stuff like this aligned storage (which they then get wrong) while they forget to put enough wheels on the car.Calisa

© 2022 - 2024 — McMap. All rights reserved.