56

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?

bitmask
  • 32,434
  • 14
  • 99
  • 159
  • 2
    The relevant proposal appears to be [P1413R3 - Deprecate std::aligned_storage and std::aligned_union](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1413r3.pdf), which was indeed [approved](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/n4907.pdf) on Feb. 7 2022 – Brian61354270 Apr 11 '22 at 13:08
  • There's a note in the standard _"Uses of aligned_­storage​::​type can be replaced by an array std​::​byte[Len] declared with alignas(Align)."_ – Passer By Apr 11 '22 at 13:12
  • 2
    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"?](https://stackoverflow.com/q/71834788/2752075) – HolyBlackCat Apr 11 '22 at 22:06

1 Answers1

58

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)...})];
};
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • 1
    Adding the "background" section improved the answer a lot, thanks. – François Andrieux Apr 11 '22 at 13:25
  • @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. – Ted Lyngmo Apr 11 '22 at 13:26
  • 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_cast`ing? – bitmask Apr 11 '22 at 13:54
  • 1
    @bitmask 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 :-) – Ted Lyngmo Apr 11 '22 at 14:08
  • 5
    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. – bitmask Apr 11 '22 at 14:34
  • @bitmask The proposal isn't particularly long or difficult, you can read it for the finer details. – Passer By Apr 11 '22 at 16:04
  • 7
    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... – Remy Lebeau Apr 11 '22 at 22:40
  • 1
    @RemyLebeau Nice idea! I'm not sure if I've walked into UB land with this [`uninitialized_array_t`](https://godbolt.org/z/Pb3qK8a3c) implementation, but I _think_ it should work - or perhaps [with `unique_ptr` support](https://godbolt.org/z/9Gfcc1s77): – Ted Lyngmo Apr 13 '22 at 14:05
  • @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_at` – Human-Compiler Apr 20 '22 at 17:29
  • @Human-Compiler Wouldn't that default initialize the `T`:s in C++23 too? (I have very little knowledge about what's coming in C++23) – Ted Lyngmo Apr 20 '22 at 18:06
  • 3
    @TedLyngmo If I understand the footnote on [this answer by Barry](https://stackoverflow.com/a/71773182/1678770), 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. – Human-Compiler Apr 20 '22 at 18:51
  • @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]``` ? – user3882729 Aug 06 '22 at 01:17
  • @user3882729 Human-Compiler wrote that, not me, but [`std::construct_at`](https://en.cppreference.com/w/cpp/memory/construct_at) is equivalent to `return ::new (const_cast(static_cast(p))) T(std::forward(args)...);` except that `construct_at` may be used in evaluation of constant expressions. – Ted Lyngmo Aug 06 '22 at 06:09
  • 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. – user3882729 Aug 06 '22 at 08:07
  • 1
    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. – metamorphosis Oct 02 '22 at 01:48
  • @metamorphosis Are you addressing one of us (if so, who) or P1413R3? – Ted Lyngmo Oct 02 '22 at 05:42
  • 2
    God I hate the C++ standard. They make everything so complicated and forget about the basics. – Bas Jan 28 '23 at 23:47
  • @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? – digito_evo Jul 10 '23 at 20:35
  • 1
    @digito_evo 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 :-) – Ted Lyngmo Jul 11 '23 at 03:09