0

(Consider this question for C++17 and forward)

LWG issue 3562(+), whether nullopt_t's requirement to not be DefaultConstructible could be superseded with the explicit explicitly-defaulted default-constructor of other tag types:

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

was closed as not a defect (NAD), with the following rationale

2021-06-14 Reflector poll:

Current wording prevents an implicit conversion sequence from {}.

(+) Note that this issue mentions both LWG issue 2510 and CWG issues 1518 and 1630, whose resolutions (changed/superseded) were tightly coupled.

Consider the following example:

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

struct S {
    constexpr S() {} // user-provided: S is not an aggregate
    constexpr S(S const&)      = default;
    S& operator=(S const&)     = default; // #1
    S& operator=(my_nullopt_t) = delete;  // #2
};

int main() {
    S s{};
    s = {};  // #3
}

which I would expect is well-formed as the overload #2 is not viable for the assignment at #3, as the default constructor of my_nullopt_t is explicit (meaning my_nullopt_t is moreover not an aggregate in C++17). I particularly thought this was governed by [over.match.ctor]/1:

When objects of class type are direct-initialized, copy-initialized from an expression of the same or a derived class type ([dcl.init]), or default-initialized, overload resolution selects the constructor. For direct-initialization or default-initialization that is not in the context of copy-initialization, the candidate functions are all the constructors of the class of the object being initialized. For copy-initialization (including default initialization in the context of copy-initialization), the candidate functions are all the converting constructors ([class.conv.ctor]) of that class. [...]

[class.conv.ctor]/1 A constructor that is not explicit ([dcl.fct.spec]) specifies a conversion from the types of its parameters (if any) to the type of its class. Such a constructor is called a converting constructor.

But given the reflector's comment above I'm probably mistaken. Is possibly [over.match.list]/1

In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed. [ Note: This differs from other situations ([over.match.ctor], [over.match.copy]), where only converting constructors are considered for copy-initialization. This restriction only applies if this initialization is part of the final result of overload resolution. — end note ]

governing this case via entry from [over.ics.list]/7?

We may finally note that we also see compiler variance here, where Clang accepts the program whereas GCC rejects it (for C++11 through C++20) with an ambiguity error. GCC particularly seems to consider #2 a viable overload, whereas Clang does not.

GCC error: ambiguous overload for 'operator='

DEMO

Question

  • What standardese passages support's the reflector's rationale above, e.g. applied to the example above?
    • Is this then in support of GCC's rejection of the program above, such that Clang is wrong to accept it?
18
  • How [over.match.ctor]/1 is related to copy-initialization from {}? {} is not an expression at all. Commented Sep 23, 2021 at 11:44
  • @LanguageLawyer Would we need to copy-construct the value type parameter in S& operator=(my_nullopt_t) (if it is to be a viable candidate)? As I mention I'm likely missing something here, and I would be really happy for an enlightening answer :) Commented Sep 23, 2021 at 11:57
  • Copy-construct? Commented Sep 23, 2021 at 12:19
  • @LanguageLawyer My (probably ill-informed) thoughts where that {} is (somehow viable) to construct a nullopt temporary object, which is used as an argument to S& operator=(my_nullopt_t), where the parameter type is a non-reference type that would arguably require to be copy-constructed from the temporary nullopt object. Is this not a copy-initialization context? But again, I'm obviously misunderstanding this, and would be grateful for an explanation of how there's an implicit conversion sequence from {} to an overload to a tag type with explicit default constructor. Commented Sep 23, 2021 at 12:29
  • (There's also the compiler variance between Clang and GCC, adding to the confusion.) Commented Sep 23, 2021 at 12:30

1 Answer 1

1

GCC is right, and Clang and MSVC are wrong. This is highlighted in:

which was also closed as NAD.

Somewhat surprisingly, the rules regarding explicit constructors differ between copy-list-initialization ([over.match.list]) and copy-initialization ([over.match.copy]) [emphasis mine]:

1228. Copy-list-initialization and explicit constructors

  • Section: 12.2.2.8 [over.match.list]
  • Status: NAD
  • Submitter: Daniel Krügler
  • Date: 2010-12-03

The rules for selecting candidate functions in copy-list-initialization (12.2.2.8 [over.match.list]) differ from those of regular copy-initialization (12.2.2.5 [over.match.copy]): the latter specify that only the converting (non-explicit) constructors are considered, while the former include all constructors but state that the program is ill-formed if an explicit constructor is selected by overload resolution. This is counterintuitive and can lead to surprising results. For example, the call to the function object p in the following example is ambiguous because the explicit constructor is a candidate for the initialization of the operator's parameter:

struct MyList {
  explicit MyStore(int initialCapacity);
};

struct MyInt {
  MyInt(int i);
};

struct Printer {
  void operator()(MyStore const& s);
  void operator()(MyInt const& i);
};

void f() {
  Printer p;
  p({23});
}

Rationale (March, 2011):

The current rules are as intended.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.