This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of Open status.

2157. How does std::array<T,0> initialization work when T is not default-constructible?

Section: 23.3.3.5 [array.zero] Status: Open Submitter: Daryle Walker Opened: 2012-05-08 Last modified: 2021-03-14

Priority: 3

View all other issues in [array.zero].

View all issues with Open status.

Discussion:

Objects of std::array<T, N> are supposed to be initialized with aggregate initialization (when not the destination of a copy or move). This clearly works when N is positive. What happens when N is zero? To continue using an (inner) set of braces for initialization, a std::array<T, 0> implementation must have an array member of at least one element, and let default initialization take care of those secret elements. This cannot work when T has a set of constructors and the default constructor is deleted from that set. Solution: Add a new paragraph in 23.3.3.5 [array.zero]:

The unspecified internal structure of array for this case shall allow initializations like:

array<T, 0> a = { };

and said initializations must be valid even when T is not default-constructible.

[2012, Portland: Move to Open]

Some discussion to understand the issue, which is that implementations currently have freedom to implement an empty array by holding a dummy element, and so might not support value initialization, which is surprising when trying to construct an empty container. However, this is not mandated, it is an unspecified implementation detail.

Jeffrey points out that the implication of 23.3.3.1 [array.overview] is that this initialization syntax must be supported by empty array objects already. This is a surprising inference that was not obvious to the room, but consensus is that the reading is accurate, so the proposed resolution is not necessary, although the increased clarity may be useful.

Further observation is that the same clause effectively implies that T must always be DefaultConstructible, regardless of N for the same reasons - as an initializer-list may not supply enough values, and the remaining elements must all be value initialized.

Concern that we are dancing angels on the head of pin, and that relying on such subtle implications in wording is not helpful. We need a clarification of the text in this area, and await wording.

[2015-02 Cologne]

DK: What was the outcome of Portland? AM: Initially we thought we already had the intended behaviour. We concluded that T must always be DefaultConstructible, but I'm not sure why. GR: It's p2 in std::array, "up to N". AM: That wording already implies that "{}" has to work when N is zero. But the wording of p2 needs to be fixed to make clear that it does not imply that T must be DefaultConstructible.

Conclusion: Update wording, revisit later.

[2015-10, Kona Saturday afternoon]

MC: How important is this? Can you not just use default construction for empty arrays?

TK: It needs to degenerate properly from a pack. STL agrees.

JW: Yes, this is important, and we have to make it work.

MC: I hate the words "initialization like".

JW: I'll reword this.

WEB: Can I ask that once JW has reworded this we move it to Review rather than Open?

MC: We'll try to review it in a telecon and hopefully get it to tentatively ready.

STL: Double braces must also work: array<T, 0> a = {{}};.

Jonathan to reword.

[2018-03-14 Wednesday evening issues processing]

Jens suggested that we remove the requirement that begin() == end() == unique-value, specifically the unique value part.

Previous resolution [SUPERSEDED]:

This wording is relative to N3376.

Add the following new paragraph between the current 23.3.3.5 [array.zero] p1 and p2:

-1- array shall provide support for the special case N == 0.

-?- The unspecified internal structure of array for this case shall allow initializations like:

array<T, 0> a = { };

and said initializations must be valid even when T is not default-constructible.

-2- In the case that N == 0, begin() == end() == unique value. The return value of data() is unspecified.

-3- The effect of calling front() or back() for a zero-sized array is undefined.

-4- Member function swap() shall have a noexcept-specification which is equivalent to noexcept(true).

[2018-06-14, Jonathan Wakely comments and provides revised wording]

The new wording does not address the 2018-03-14 suggestion from Jens to remove the unique value. It wasn't clear to me that there was consensus to make that change, and it would be a change in behaviour not just a clarification of the existing wording.

Previous resolution [SUPERSEDED]:

This wording is relative to N4750.

Modify 23.3.3.5 [array.zero] as indicated:

-1- array shall provides support for the special case of a zero-sized array that is always empty, i.e. N == 0, with the properties described in this subclause.

-?- A zero-sized array type is an aggregate that meets the DefaultConstructible (Table 22) and CopyConstructible (Table 24) requirements. There is a single element of the aggregate, of an unspecified DefaultConstructible type. [Note: This allows initialization of the form array<T, 0> a = {{}};. There is no requirement for T to be DefaultConstructible. — end note]

-2- In the case that N == 0, begin() == end() == unique valuebegin() and end() return non-dereferenceable iterators such that begin() == end() and a.begin() != b.begin() where a and b are distinct objects of the same zero-sized array type. The return value of data() is unspecified.

-3- The effect of calling front() or back() for a zero-sized array is undefined.

-4- Member function swap() shall havehas constant complexity and a non-throwing exception specification.

[2018-08-30, Jonathan revises wording following feedback from Daniel Kruegler and Tim Song.]

Daniel noted that it's undefined to compare iterators from different containers, so a.begin() != b.begin() can't be used. That means whether the iterators from different containers are unique is unobservable anyway. We can say they don't share the same underlying sequence, which tells users they can't compare them and tells implementors they can't return value-initialized iterators.
Tim noted that it's not sufficient to say the unspecified type in a zero-sized array is DefaultConstructible, it also needs to be constructible from = {}. Also, a zero-sized array should be CopyAssignable.

Previous resolution [SUPERSEDED]:

This wording is relative to N4762.

Modify 23.3.3.5 [array.zero] as indicated:

-1- array shall provides support for the special case of a zero-sized array that is always empty, i.e. N == 0, with the properties described in this subclause.

-?- A zero-sized array type is an aggregate that meets the Cpp17DefaultConstructible (Table 24) and Cpp17CopyConstructible (Table 26) and Cpp17CopyAssignable (Table 28) requirements. There is a single element of the aggregate, of an unspecified Cpp17DefaultConstructible type that is copy-list-initializable from an empty list. [Note: This allows initialization of the form array<T, 0> a = {{}};. There is no requirement for T to be Cpp17DefaultConstructible. — end note]

-2- In the case that N == 0, begin() == end() == unique valuebegin() and end() return non-dereferenceable iterators such that begin() == end(). When a and b are distinct objects of the same zero-sized array type, a.begin() and b.begin() are not iterators over the same underlying sequence. [Note: Therefore begin() does not return a value-initialized iterator — end note]. The return value of data() is unspecified.

-3- The effect of calling front() or back() for a zero-sized array is undefined.

-4- Member function swap() shall havehas constant complexity and a non-throwing exception specification.

[2021-03-14; Johel Ernesto Guerrero Peña comments and provides improved wording]

The currently proposed wording specifies:

There is a single element of the aggregate, of an unspecified Cpp17DefaultConstructible type that is copy-list-initializable from an empty list.

This doesn't specify which expressions involving zero-sized array specializations are constant expressions. 23.3.3.1 [array.overview] p4 specifies array<T, 0> to be a structural type when T is a structural type. This requires that its single element, let's call it single-element, be a structural type. But that says nothing about which of the special member functions of single-element are constant expressions. By being a structural type, single-element is permitted to be implemented as a literal class type. To meet this requirement, single-element can be implemented to have one constexpr constructor that is not a copy or move constructor (6.8.1 [basic.types.general] p10), so its default constructor needn't be constexpr. This is unlike non-zero-sized array specializations, which inherit these properties from T. Furthermore, this permits an implementation of single-element whose default constructor stores the result of std::source_location::current() in a data member (as exemplified in the specification for current). Cpp17DefaultConstructible doesn't require the default constructor to produce equal values. The simplest way to solve these issues and any other that might arise from future changes and oversights would be to specify single-element as an empty aggregate type. Then the wording from 23.3.3.2 [array.cons] p1 makes it clear that all the special member functions are constant expressions. It would also mean that the default constructor produces template-argument-equivalent values.

Proposed resolution:

This wording is relative to N4878.

Modify 23.3.3.5 [array.zero] as indicated:

  1. -1- array shall provides support for the special case of a zero-sized array that is always empty, i.e. N == 0, with the properties described in this subclause.

    -?- A zero-sized array type is an aggregate that meets the Cpp17DefaultConstructible (Table 29 [tab:cpp17.defaultconstructible]) and Cpp17CopyConstructible (Table 31 [tab:cpp17.copyconstructible]) and Cpp17CopyAssignable (Table 33 [tab:cpp17.copyassignable]) requirements. There is a single element of the aggregate, of an unspecified empty aggregate type. [Note: This allows initialization of the form array<T, 0> a = {{}};. There is no requirement for T to be Cpp17DefaultConstructible. — end note]

    -2- In the case that N == 0, begin() == end() == unique valuebegin() and end() return non-dereferenceable iterators such that begin() == end(). When a and b are distinct objects of the same zero-sized array type, a.begin() and b.begin() are not iterators over the same underlying sequence. [Note: Therefore begin() does not return a value-initialized iterator — end note].. The return value of data() is unspecified.

    -3- The effect of calling front() or back() for a zero-sized array is undefined.

    -4- Member function swap() shall havehas constant complexity and a non-throwing exception specification.