Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compiler variance for ambiguous copy-assignment from empty-braces

I've been trying to understand the rationale for std::nullopt_t to not be allowed to be DefaultConstructible in C++17 (where it was introduced) and beyond, and stepped over some compiler variance confusion in the process.

Consider the following spec-violating (it is DefaultConstructible) implementation of nullopt_t:

struct nullopt_t {
    explicit constexpr nullopt_t() = default;
};

which is an aggregate in C++11 and C++14 (no user-provided ctors), but which is not an aggregate in C++17 (explicit ctor) and C++20 (user-declared ctor).

Now consider the following example:

struct S {
    constexpr S() {}
    S(S const&) {}
    S& operator=(S const&) { return *this; }   // #1
    S& operator=(nullopt_t) { return *this; }  // #2
};

int main() {
    S s{};
    s = {};  // GCC error: ambiguous overload for 'operator=' (#1 and #2)
}

This is rejected by GCC (various versions, say v11.0) throughout C++11 to C++20, but is accepted by both Clang (say v12.0) and MSVC (v19.28) throughout C++11 to C++20.

DEMO

My initial assumptions were that the program:

  • is ill-formed in C++11 and C++14, as nullopt_t (as above) is an aggregate, whereas it
  • is well-formed in C++17 and C++20, as it is no longer an aggregate, meaning that its explicit default constructor should prohibit copy-list-init of a temporary nullopt_t object as needed for the copy assignment operator at #2 to be viable,

but none of the compilers agree in full with this theory, some I'm probably missing something.

What compiler is correct here (if any), and how do we explain it by relevant standard sections (and DR:s, if relevant)?

like image 847
dfrib Avatar asked May 10 '21 10:05

dfrib


Video Answer


1 Answers

Why is nullopt_t required to be DefaultConstructible in the first place?

The spec requirement that nullopt_t shall not be DefaultConstructible is arguably, in retrospect, a mistake based on some LWG and CWG confusion around tag types, and the resolution of this confusion which came only after std::optional was brought in from the Library Fundamentals TS Components.

First of all, the current (C++17, C++20) spec of nullopt_t, [optional.nullopt]/2, requires [emphasis mine]:

Type nullopt_­t shall not have a default constructor or an initializer-list constructor, and shall not be an aggregate.

and its main use is described in the previous section, [optional.nullopt]/1:

[...] In particular, optional<T> has a constructor with nullopt_­t as a single argument; this indicates that an optional object not containing a value shall be constructed.

Now, P0032R3 (Homogeneous interface for variant, any and optional), one of the papers which was part of introducing std::optional, has a discussion around nullopt_t, tag types in general, and the DefaultConstructible requirement [emphasis mine]:

No default constructible

While adapting optional<T> to the new in_place_t type we found that we cannot anymore use in_place_t{}. The authors don't consider this a big limitation as the user can use in_place instead. It needs to be noted that this is in line with the behavior of nullopt_t as nullopt_t{} fails as no default constructible. However nullptr_t{} seems to be well formed.

Not assignable from {}

After a deeper analysis we found also that the old in_place_t supported in_place_t t = {};. The authors don't consider this a big limitation as we don't expect that a lot of users could use this and the user can use in_place instead.

in_place_t t;
t = in_place;

It needs to be noted that this is in line with the behavior of nullopt_t as the following compile fails.

nullopt_t t = {}; // compile fails

However nullptr_t seems to be support it.

nullptr_t t = {}; // compile pass

To re-enforce this design, there is an pending issue 2510-Tag types should not be DefaultConstructible Core issue 2510.

And indeed, the initial proposed resolution of LWG Core Issue 2510 was to require all tag types to not be DefaultConstructible [emphasis mine]:

(LWG) 2510. Tag types should not be DefaultConstructible

[...]

Previous resolution [SUPERSEDED]:

[...] Add a new paragraph after 20.2 [utility]/2 (following the header synopsis):

  • -?- Type piecewise_construct_t shall not have a default constructor. It shall be a literal type. Constant piecewise_construct shall be initialized with an argument of literal type.

This resolution was superseded, however, as there were overlap with CWG Core Issue 1518, which was eventually resolved in a way that did not require tag types to not be DefaultConstructible, as explicit would suffice [emphasis mine]:

(CWG) 1518. Explicit default constructors and copy-list-initialization

[...]

Additional note, October, 2015:

It has been suggested that the resolution of issue 1630 went too far in allowing use of explicit constructors for default initialization, and that default initialization should be considered to model copy initialization instead. The resolution of this issue would provide an opportunity to adjust that.

Proposed resolution (October, 2015):

Change 12.2.2.4 [over.match.ctor] paragraph 1 as follows:

[...] For direct-initialization or default-initialization, the candidate functions are all the constructors of the class of the object being initialized. [...]

as long as explicit also implied that the type was not an aggregate, which in turn was the final resolution of LWG Core Issue 2510 (based on the final resolution of CWG Core Issue 1518)

(LWG) 2510. Tag types should not be DefaultConstructible

[...]

Proposed resolution:

[...] In 20.2 [utility]/2, change the header synopsis:

  • // 20.3.5, pair piecewise construction
    struct piecewise_construct_t { explicit piecewise_construct_t() = default; };
    constexpr piecewise_construct_t piecewise_construct{};
    

[...]

These latter changes, however, were not brought into the proposal for std::optional, arguably an oversight, and I would like to claim that nullopt_t need not be required to not be DefaultConstructible, only, like other tag types, that it should have a user-declared explicit constructor, which bans it from a candidate for empty-braces copy-list-init both by it not being an aggregate and by the only candidate constructor being explicit.

Which compiler is right and wrong here?

Given the LWG 2510, CWG 1518 (and other) confusion, let's focus on C++17 and beyond. In this case, GCC is arguably wrong to reject the program, whereas Clang and MSVC are correct to accept it.

Why?

Because the S& operator=(nullopt_t) assignment operator is not viable for the assignment s = {};, as the empty braces {} would require either aggregate initialization or copy-list-initialization to create a nullopt_t (temporary) object. nullopt_t, however (by the idiomatic tag implementation: my implementation above), as per as per P0398R0 (which resolves CWG Core Issue 1518), is neither an aggregate nor does its default constructor participate in copy-list-initialization (from empty braces).

This likely falls under the following GCC bug report:

  • Bug 54835 - (C++11)(DR 1518) Explicit default constructors not respected during copy-list-initialization

which was listed as SUSPENDED in 2015-06-15, before the change in the resolution of CWG Core Issue 1630 ("resolution of issue 1630 went too far"). The ticket is now re-opened based on a ping from this Q&A.

like image 67
dfrib Avatar answered Oct 30 '22 12:10

dfrib