r/wg21
P3737R3 - std::array is a wrapper for an array! WG21
Posted by u/array_wrapper_advocate · 8 hr. ago

Document: P3737R3
Author: Jan Schultke
Date: 2026-02-22
Audience: LEWG

We all teach that std::array is just a wrapper for a C-style array. But the standard doesn't actually say that. The spec is permissive enough that a compliant implementation could add extra members, break trivial copyability, or inflate the size. This paper makes the simplified explanation true by standardizing what every implementation already does: a single public data member of type T[N] for nonzero N.

The zero-length case is where it gets interesting. MSVC STL's std::array<T, 0> actually holds a T and calls its constructors - a known bug that's an ABI break to fix. The paper standardizes the libstdc++/libc++ behavior: trivially copyable, non-empty, with a placeholder member. Also deletes the never-implemented "unique value" iterator requirement for empty arrays.

▲ 156 points (94% upvoted) · 24 comments
sorted by: best
u/bit_cast_enjoyer 127 points 7 hr. ago 🏆

I've been using std::bit_cast<std::array<std::byte, sizeof(T)>>(x) for years assuming it works because std::array has the same layout as T[N]. Turns out that was technically relying on non-standard behavior. This paper makes it legal. Should have been done ten years ago.

u/ub_detector_9000 64 points 6 hr. ago

Welcome to C++ standardization, where the things everyone assumes are true have been technically UB for a decade and nobody noticed because every implementation did the obvious thing.

u/msvc_stl_watcher 89 points 7 hr. ago

The MSVC STL section is brutal.

The MSVC STL implementation is non-compliant. Despite std::array<T, 0> being a zero-length container with no elements, it actually holds one element (and calls its constructors and destructors) as long as T is default-constructible.

And fixing it is an ABI break. STL#5583 has been open since 2025. The paper basically says "when MSVC eventually breaks ABI, here's what to change to."

u/abi_break_enthusiast 52 points 6 hr. ago

ABI stability: protecting the bugs of yesterday at the expense of the correctness of tomorrow.

u/trivially_copyable_matters 72 points 6 hr. ago

Making std::array<T, 0> unconditionally trivially copyable is the right call. Both libc++ and libstdc++ already do this. The alternative - making it conditional on T - adds complexity with no benefit. An empty container with no elements shouldn't care whether T has a non-trivial copy constructor.

u/generic_programming_pedant 28 points 5 hr. ago

Counter-argument: it's surprising that std::is_trivially_copyable_v<std::array<std::string, 0>> would be true when std::is_trivially_copyable_v<std::string> is false. Generic code that branches on trivial copyability might get confused.

u/trivially_copyable_matters 19 points 4 hr. ago

What would the generic code do differently? memcpy on a zero-element container copies zero elements regardless. Trivial copyability of an empty container is a degenerate case that can't produce wrong results.

u/malice_and_evil 58 points 7 hr. ago

The "malice_and_evil" example in Section 2.2 is perfect. A technically compliant std::array with alignas(1024) and a non-trivially-copyable extra member. Nobody would ship this. But the standard allows it. The paper's thesis in one struct:

If the remaining implementation freedom can only be used for evil, perhaps we should not grant it.
u/iterator_spec_lawyer 41 points 5 hr. ago

The deletion of the "unique value" iterator requirement for zero-length arrays (Section 4.4) is long overdue. The requirement was never implemented by any compiler. All three major standard libraries use T* iterators, making begin() == end() trivially true. The spec said something nobody did, and now the spec will stop saying it.

u/structural_type_enjoyer 34 points 6 hr. ago

Making the structural type property follow naturally from the data member specification is elegant. The current standard has a special paragraph for it. After this paper, it's just a consequence of the type having a single public member of type T[N]. Less spec text, same guarantee.

u/double_brace_init_fan 27 points 5 hr. ago

TIL that std::array<int, 0> {{}} needs a non-static data member to work (otherwise double-brace initialization fails). This is why the zero-length specialization can't be an empty class - it needs something to initialize. The things you learn reading wording papers.

u/lwg_issue_archaeologist 22 points 4 hr. ago

LWG2157 has been open since 2012. Fourteen years. This paper finally absorbs it. Some bugs are patient.

u/greatest_common_denominator 18 points 4 hr. ago

The design strategy is just "standardize the greatest common denominator of libc++ and libstdc++." No new behavior. No controversial choices. Just write down what everyone already does. More papers should be this boring and this useful.

u/to_array_double_braces 14 points 3 hr. ago

The std::to_array return specification uses {{ a[0], ..., a[N - 1] }} - double braces. If std::array weren't a wrapper for a C array, that specification would be nonsensical. The paper points out the standard was already implicitly assuming this layout.

u/implicit_object_creation 11 points 2 hr. ago

Section 3 mentions implicit object creation. Having the guarantee that std::array contains an actual array means [intro.object] rules for providing storage and implicit lifetime apply. Without this paper, it's unclear whether std::array<std::byte, N> can serve as storage for implicit-lifetime types. With it, the answer is yes.

u/gobbler_constructor 8 points 1 hr. ago

The "gobbler" example - a compliant std::array implementation that swallows list-initialization into a separate member - is the kind of adversarial spec reading that makes you question everything you thought you knew about the standard library.

u/data_returns_arr 5 points 42 minutes ago

The new data() specification just says "Returns: arr." Two words. Replacing a paragraph about valid ranges and addressof(front()). The simplification is beautiful.