std :: array
is a wrapper for an array!
- Document number:
- P3737R0
- Date:
2025-06-08 - Audience:
- LEWG
- Project:
- ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
- Reply-to:
- Jan Schultke <janschultke@gmail.com>
- GitHub Issue:
- wg21.link/P3737/github
- Source:
- github.com/Eisenwave/cpp-proposals/blob/master/src/array.cow
class template is implemented as a simple wrapper type
for a "C-style array".
However, its specification in the standard is considerably more permissive
and should be simplified.
Contents
Introduction
What the standard says
What the standard does not say
Motivation
Isn't this a waste of time?
Design considerations
Zero-length std :: array
status quo
Conclusion
Trivial copyability of zero-length arrays
Conclusion
Double-brace initialization for zero-length arrays
Conclusion
Problematic iterator requirements for zero-length arrays
Conclusion
std :: array < T , 0 > :: front ( )
is std :: unreachable ( )
?!
Conclusion
Impact on implementations
Wording
[array.overview]
[array.zero]
References
1. Introduction
The
class template has established itself
as a de-facto replacement for "builtin arrays" or "C-style arrays" in many code bases.
This also means that it is frequently taught to novice programmers,
with an explanation along the lines of:
is just a wrapper for a C-style array:
std :: array template < typename T , size_t N > struct array { T __array [ N ] ; // ... }
While this explanation is not correct for zero-length
s,
it does match how the template is implemented in every standard library for
,
and there is very little reason not to implemented it in this obvious fashion.
1.1. What the standard says
The actual specification of
is not so simple,
and is a combination of multiple constraints on the implementation:
- It is a class template, a contiguous container ([array.overview] paragraph 1), and a reversible container (with an exception; see [array.overview] paragraph 3), and it meets some requirements of a sequence container.
-
It can be list-initialized with up to N elements
whose types are convertible to
([array.overview] paragraph 2). This is obviously not exhaustive; initialization withT
or{ }
should also be possible.{ other_array } -
It is a structural type if
is a structural type ([array.overview] paragraph 4), and therefore, also a literal class type in that event.T
Additionally, while this does not strictly specify anything about the layout, some helper functions in the standard library de-facto rely on it. Take [array.creation] for example:
template < class T , size_t N > constexpr array < remove_cv_t < T > , N > to_array ( T ( & a ) [ N ] ) ; Mandates:
is
is_array_v < T > and
false is
is_constructible_v < remove_cv_t < T > , T & > .
true Preconditions:
meets the Cpp17CopyConstructible requirements.
T Returns:
{ { a [ 0 ] , …,a [ N - 1 ] } }
The use of double braces in the Returns specification
would be nonsensical if
was not
"a wrapper for an array".
1.2. What the standard does not say
Notably, there are quite a couple of guarantees that are absent.
as follows:
Such an implementation technically satisfies all the requirements for
, but
- copying the array would not be a constant expression,
would not be trivially copyable for any type, andstd :: array - its size and alignment would be much greater than that of
.T [ N ]
Since [array] never states what effect list-initialization has for a
,
and even
is just stated to return the result of some expression,
nothing suggests that
would give us an iterator to
after initializing like
.
All list-initialization could be "gobbled up".
2. Motivation
It seems like the vagueness in the specification serves no practical purpose; it is unclear what implementations could do with the additional freedom, other than pranking their users. It would be beneficial to the C++ community if the simplified explanation in §1. Introduction was what the standard actually said.
A stricter specification would provide additional useful guarantees
such as
being trivially copyable when
is trivially copyable.
This is relevant to use cases like
,
which technically rely on implementation details, not on standard behavior.
2.1. Isn't this a waste of time?
While it could be argued that only a malicious implementation would violate our user
expectations as in §1.2. What the standard does not say and it is therefore time-wasting to restrict
any further,
it would be unusual for WG21 to shy away from standardizing universally existing practice
and to recommend users to rely on non-standard implementation details,
simply because those implementation details are widespread.
If the remaining implementation freedom can only be used for evil,
perhaps we should not grant it.
3. Design considerations
While the specification for arrays of nonzero length is rather obvious,
it is unclear how many guarantees we want to provide for zero-length arrays.
For example, should
be trivially copyable,
even though
is not?
Within [array.zero], there are some long-standing issues going back to 2012. LWG has visited this subclause many times in [LWG2157], but never fully completed a solution. This work has been absorbed mostly unmodified into §5. Wording.
3.1. Zero-length std :: array
status quo
The zero-length case is also where we see some implementation divergence in size and alignment
of the array.
The following table shows how major standard libraries implement zero-length
.
Library | Implementation | Size | Trivially copyable | Assignable |
---|---|---|---|---|
MSVC STL | contains if is default-constructible,otherwise
|
or
|
depends on
|
depends on
|
libstdc++ | contains
|
|
always | always |
libc++ | contains (possibly const)
|
|
always | depends on
|
being a zero-length container with
no elements,
it will actually hold one element (and call its constructors and destructors)
as long as
is default-constructible.
3.1.1. Conclusion
Generally speaking,
it is desirable if a zero-length
behaves as similarly
to a regular
of the same element type.
libc++ is the only implementation that does this well.
The "greatest common denominator" between these implementations should be standardized, which is:
is trivially copyable ifstd :: array < T , 0 >
is.T
is assignable ifstd :: array < T , 0 >
is.T
has size and alignment at most that ofstd :: array < T , 0 >
.T
is not an empty class.std :: array < T , 0 >
Without breaking ABI, that implementation would look something like:
3.2. Trivial copyability of zero-length arrays
Whether a type is trivially copyable has ABI impact. It may change whether the type is passed via register or one the stack, and so we cannot mandate any change to this behavior without breaking ABI.
For MSVC, a
is trivially copyable,
but a
is not.
This seems reasonable;
there is no strong motivation for making arrays trivially copyable even if
a nonzero variant of the same array wouldn't have been.
In fact, it could be argued that this is surprising and inconsistent.
3.2.1. Conclusion
Mandate that
is trivially copyable if
is.
Otherwise leave this up to implementations.
3.3. Double-brace initialization for zero-length arrays
Note that we need to make double-brace initialization like
valid to make generic programming easier.
It is plausible that we perform this when expanding an empty pack like:
.
Making this valid requires either some non-static data member, or a base class. An empty base class would make the array as a whole an empty class, and this would be an ABI break, so it is out of the question.
3.3.1. Conclusion
Standardize the existing practice of having a non-static data member which enables double-brace initialization.
3.4. Problematic iterator requirements for zero-length arrays
[array.zero] paragraph 2 specifies:
In the case that
,
N == 0 unique value. The return value of
begin ( ) == end ( ) == is unspecified.
data ( )
Firstly, it is unclear whether this "unique value" is meant to be unique per object, unique for each invocation, etc.
Secondly, this requirement was never implemented by any compiler and it is too late to fix now.
Note that MSVC STL, libc++, and libstdc++ all use
as an iterator type.
Considering that, a possible implementation looks like:
However, this would require
to be at least one
large,
and it is only a single byte large for libstdc++.
Changing the size of the type would break ABI.
The only way to conjure up a
out of thin air would be to use
, but that would not work in constant expressions.
3.4.1. Conclusion
Delete the uniqueness requirement.
Without specifying anything special for zero-length arrays,
it still acts as an empty range, and
is
,
which is all we really need.
3.5. std :: array < T , 0 > :: front ( )
is std :: unreachable ( )
?!
is entirely undefined,
making it equivalent to
,
which feels out-of-place especially following C++26,
where
normally has a Hardened preconditions specification.
Note that deleting
and
for zero-length arrays is not feasible because
of (existing) code along the lines of:
It is quite plausible that
is called in code
that is logically unreachable,
so it should not result in a compiler error, which would be the consequence of
.
3.5.1. Conclusion
Make
and
always result in a contract violation,
as if they had Hardened preconditions that are always violated.
However, if the implementation is not hardened,
these functions should simply terminate instead of being another spelling for
.
4. Impact on implementations
Existing implementations of
are virtually unaffected.
One exception to this is that the behavior of
is no longer undefined;
see below for specifics.
5. Wording
The following changes are relative to [N5008].
5.1. [array.overview]
Change [array.overview] paragraph 1 as follows:
The header An
An
is a contiguous container ([container.reqmts]).instance object of type
stores
elements of type
, so that
is an invariant
always equals
.
Change [array.overview] paragraph 2 as follows:
An
is an aggregate ([dcl.init.aggr]) that can be
list-initialized with up
to
with no base classes.
A specialization
elements whose types are convertible to
has a single public non-static data member of type
"array of
"
if
is nonzero;
otherwise the contents are specified in [array.zero].
is trivially copyable, standard-layout, and a structural type
if
is trivially copyable, standard-layout,
and a structural type, respectively.
— end note]
Change [array.overview] paragraph 3 as follows:
An
meets all of the requirements
of a container ([container.reqmts]),
of a contiguous container, and
of a reversible container ([container.rev.reqmts]),
except that a
default constructed
default-initialized or value-initialized object of type
object
is not empty if
> 0.
An
meets some of the requirements of a sequence
container ([sequence.reqmts]).
Descriptions are provided here
only for operations on
that are not described in
one of these tables, and
for operations where there is additional semantic information.
Delete [array.overview] paragraph 4:
is a structural type ([term.structural.type]) if
is a structural type.
Two values
and
of type
are template-argument-equivalent ([temp.type]) if and only if
each pair of corresponding elements in
and
are template-argument-equivalent.
Change [array.overview] paragraph 5 as follows:
5.2. [array.zero]
Delete all paragraphs within [array.zero]:
1
shall provide support for the special case
.
2
In the case that
,
unique value.
The return value of
is unspecified.
3
The effect of calling
or
for a zero-sized array is
undefined.
4
Member function
shall have a
non-throwing exception specification.
Insert new paragraphs within [array.zero]:
1
A specialization
does not have an
with no base classes
and with the same cv-qualification as
.
The size and alignment of
is an implementation-defined choice between
and
the size and alignment of
.
2
The value representation of an
is empty.
3
The
,
,
,
,
,
,
and
member functions of an
return value-initialized results.
4
The
and
member functions
of an
are equivalent to functions with a
,
and have a non-throwing exception specification.
5
is invoked when execution reaches the end of
member functions
,
, or
of an
.