Skip to content

Conversation

@CaseyCarter
Copy link
Contributor

@CaseyCarter CaseyCarter commented Oct 22, 2021

Previously, these types were non-instantiable; we can't break ABI for something that doesn't compile. (Why do I always end up coding the PRs that fire salvos at the ODR?)

Fixes #942
Fixes VSO-207715 / AB#207715.

Note that the proposed resolution of LWG-2157 clarifies how array<T, 0> should behave. It hasn't been merged yet due to many of us despising the bit about "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]". If begin and end cannot return a value-initialized iterator (either a nullptr or a wrapper around a nullptr) it's not clear how they are to be implemented so as to be usable in constexpr context.

After applying this change, we will (at least in some cases) agree with the behavior of libc++ and libstdc++ which both return iterator{data()} with data() being nullptr. I will use this as evidence that the implementations don't agree with the cited bit of LWG-2157 and hopefully convince LWG to yoink that and merge the rest.

Previously, these types were non-instantiable; we can't break ABI for something that doesn't compile.

Fixes microsoft#942
@CaseyCarter CaseyCarter added the bug Something isn't working label Oct 22, 2021
stl/inc/array Outdated
}

_Ty _Elems[1];
conditional_t<is_default_constructible_v<_Ty>, _Ty, char> _Elems[1];
Copy link
Contributor Author

@CaseyCarter CaseyCarter Oct 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could just as well be a char as char[1] - the only requirement is that {} is a valid initializer. (If we were breaking ABI, I'd have array<T, 0> inherit from an empty base type so it could be empty and still initializable with {{}}. Such a beautiful hack!)

Anyone see a reason to prefer either char or char[1]?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer something with [[msvc:no_unique_address]], if clang had that.

And I prefer char[1], as it is, for no particular reason, just because it is an array

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer something with [[msvc:no_unique_address]], if clang had that.

Good point! An empty aggregate member would work with [[no_unique_address]] in ABI-broken world.

And I prefer char[1], as it is, for no particular reason, just because it is an array

Yeah, I have vague concerns that code "somewhere" outside this header expects array::_Elems to exist and have array type.

@CaseyCarter CaseyCarter marked this pull request as ready for review October 22, 2021 19:51
@CaseyCarter CaseyCarter requested a review from a team as a code owner October 22, 2021 19:51
Copy link
Contributor

@miscco miscco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hate array<T, 0> even more than I fear input_iterator

@AlexGuteniev
Copy link
Contributor

Why do I always end up coding the PRs that fire salvos at the ODR?

Because you're an STL maintainer, Casey.

@timsong-cpp
Copy link
Contributor

timsong-cpp commented Oct 23, 2021

Previously, these types were non-instantiable; we can't break ABI for something that doesn't compile. (Why do I always end up coding the PRs that fire salvos at the ODR?)

I don't think "non-instantiable" is correct.

#include <array>

struct A { const int i; };

static_assert(not std::is_default_constructible_v<A>);
// std::array<A, 0> a; // Error, but...
std::array<A, 0> a = {}; // OK

Also, it'd be nice if copying a default-initialized array of zero doesn't give me core language undefined behavior from copying an uninitialized char.

@CaseyCarter
Copy link
Contributor Author

CaseyCarter commented Oct 25, 2021

Previously, these types were non-instantiable; we can't break ABI for something that doesn't compile. (Why do I always end up coding the PRs that fire salvos at the ODR?)

I don't think "non-instantiable" is correct.

Thank you for the reminder that default-constructible doesn't mean the same thing as default-initializable! I've adjusted the new behavior so it's enabled only when the array element type cannot be copy-initialized with {}.

Also, it'd be nice if copying a default-initialized array of zero doesn't give me core language undefined behavior from copying an uninitialized char.

Fair enough.

stl/inc/array Outdated
}

_Ty _Elems[1];
conditional_t<_Is_implicitly_default_constructible<_Ty>::value, _Ty, _Empty_array_element> _Elems[1];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to check either? If is_default_constructible is true then array<T, 0> could be default-initialized (or value-initialized), while if _Is_implicitly_default_constructible is true then array<T, 0> could be copy-list-initialized from {}. Either way you have ABI issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a hard time believing that default-constructible types that are not {}-constructible are so common that someone out there has an array of them compiled into an object file, but since you went to the trouble to make the comment I'll make the change.

@MikeGitb
Copy link

MikeGitb commented Oct 25, 2021

After applying this change, we will (at least in some cases) agree with the behavior of libc++ and libstdc++ which both return iterator{data()} with data() being nullptr.

I guess it makes sense to follow what other implementations do, but wouldn't it be much more consistent if it data would point to the start of the array object itself, just like for any other std::array type? Asked differently: What is the motivation to return a nullptr there (assuming you know)?

@CaseyCarter
Copy link
Contributor Author

After applying this change, we will (at least in some cases) agree with the behavior of libc++ and libstdc++ which both return iterator{data()} with data() being nullptr.

I guess it makes sense to follow what other implementations do, but wouldn't it be much more consistent if it data would point to the start of the array object itself, just like for any other std::array type? Asked differently: What is the motivation to return a nullptr there (assuming you know)?

Returning something other than nullptr from data at constexpr time requires that you have storage for an actual T in a union. Ideally array<T, 0> would be an empty, trivially-copyable type, but carrying a T in a union means it must have at least the size and alignment of a T, and means trivial-copyability can be at best conditional. Using nullptr simplifies the implementation, and makes it easier to catch bugs where the user calls front/back or otherwise dereferences begin() since a null pointer value is very obviously invalid.

@MikeGitb
Copy link

Returning something other than nullptr from data at constexpr time requires that you have storage for an actual T in a union.

Right, I forgot about constexpr and that T arr[0] isn't allowed. Thanks for the explanation.

and makes it easier to catch bugs where the user calls front/back

Shouldn't those trigger a compilation error?

@CaseyCarter
Copy link
Contributor Author

and makes it easier to catch bugs where the user calls front/back

Shouldn't those trigger a compilation error?

WG21 tries to avoid API discontinuity for particular template specializations to make it easier to write generic code. For example, you might write if (!meow.empty()) { do_stuff_with(meow.front()); } in a function that generically accepts sequence containers. This fails to compile if meow is an array-of-zero and front is ill-formed, but is perfectly correct if front is well-formed but can't be called.

@StephanTLavavej
Copy link
Member

I'm mirroring this to the MSVC-internal repo - please notify me if any further changes are pushed, or if more work is required.

@StephanTLavavej StephanTLavavej merged commit 793713e into microsoft:main Nov 13, 2021
@StephanTLavavej
Copy link
Member

Thanks for fixing this previously-thought-to-be-impossible bug! 🚀 🪐 😸

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

<array>: std::array<T,0> doesn't compile - when type is not default constructible

7 participants