1

Please see the following code:

struct X;

struct Y {
  Y() {}
  Y(X&) = delete;
};

struct X {
  X() {}
  operator Y() {
    return{};
  }
};

int main() {
  X x;
  static_cast<Y>(x);
}

Here, the Y's constructor taking an X is explicitly deleted, while X has a conversion operator into Y. Among these directly contradicting two, it seems that =delete always win; I tested on some recent versions of GCC, Clang, and VC++.

The question: is it the "right" behavior? I thought there is no particular precedence between conversion constructor and conversion operator, so the code above should produce an overload resolution ambiguity error. But it doesn't. It complains about usage of the deleted function. Is it because of the guaranteed copy elision?

I googled and found Conversion constructor vs. conversion operator: precedence. In that question, the conversion operator has been chosen because it was a better match due to presence of const in the conversion constructor. However, in my case replacing Y(X&) to Y(X const&) changed nothing.


Actually, the situation I want to have is the following:

X x;
Y y1(x);                  // Error
Y y2 = static_cast<Y>(x); // OK

Yes, one may call this silly, but indeed there are built-in types that behave just like that: substitute X <- int&, Y <- int&&. Inability to make a user-defined type that exactly mimics a built-in reference type seems to be a really desperately missing piece in current C++...

Junekey Jeon
  • 1,496
  • 1
  • 11
  • 18
  • Don't you already have a situation that you want to have? Note that you can invoke conversion operator directly like `Y y2 = x.operator Y();` – user7860670 Jun 29 '18 at 20:35

2 Answers2

2

From the standard 11.6.17.6.2

if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered.

Then the standard tells us that (11.6.16)

The initialization that occurs in the forms [...] as well as in new expressions (8.5.2.4), static_cast expressions (8.5.1.9), functional notation type conversions (8.5.1.3), mem-initializers (15.6.2), and the braced-init-list form of a condition is called direct-initialization.

Your example initializes a temporary via static_cast, therefore the compiler is allowed to only use constructors because of direct-initialization, hence you get an error.

An0num0us
  • 961
  • 7
  • 15
2

The question: is it the "right" behavior? I thought there is no particular precedence between conversion constructor and conversion operator [...]

That's not quite right. You're looking at the code as-is:

struct Y {
  Y() {}
  Y(X&) = delete;
};

But really there's something more there. To the compiler, Y looks like:

struct Y {
  Y() {}
  Y(X&) = delete;

  Y(Y&&) = default;
  Y(Y const&) = default;
};

The choice here isn't between Y(X&) and X::operator Y(). The choice is mainly between Y(X&) and Y(Y&&). And the former is a better match than the latter (regardless of, as you mention in the question, it's X& or X const& as the parameter). But it's deleted, so the conversion is ill-formed.


If we were copy-initializing instead of direct-initializing:

Y y = x;

Then both would be equally viable (and hence ambiguous). And yes, you really want that to be ambiguous. = delete does not remove from the candidate set!

Changing the constructor from Y(X&) to Y(X const&) would have the conversion function preferred.


Yes, one may call this silly, but indeed there are built-in types that behave just like that: substitute X <- int&, Y <- int&&

Yes, but in the original example, X and Y are different types. Here, they represent different value categories of the same type. The reason for this new example working or not working are entirely different:

X x;
Y y1(x);                  // Error
Y y2 = static_cast<Y>(x); // OK

is really:

int& x = ...;
int&& y(x);                       // error, can't bind rvalue reference to lvalue
int&& y2 = static_cast<int&&>(x); // ok. this is exactly std::move(x)

Reference binding to reference-compatible types isn't the same kind of question as conversion precedence.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Very clearly explained! Thank you. So, since both ````static_cast(x)```` and ````Y y(x)```` just equally are cases of direct-initialization, there is no way to discriminate them, right? – Junekey Jeon Jun 30 '18 at 04:08