Implicit conversions and different behavior of compilers
Asked Answered
F

1

8

Motivated by this question1, I created the following code:

struct X {
   X(int) {}
};

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

int main() {
   X x(Y{});
}

Live demo with error messages.

This code:

  • throws an ambiguity error with all GCC and Clang versions (I have tried) if C++11/14 is enabled,

  • throws an ambiguity error with some GCC and Clang versions if C++17 is enabled (e.g., GCC 8.2, GCC 6.3, Clang 5),

  • compiles with some GCC and Clang version if C++17 is enabled (e.g., GCC 7.3, Clang 6).

The ambiguity stems from possible conversions:

  1. YX,
  2. Yint and intX,

since the compiler does not know which constructor of X it should use (copy/move or converting from int).

I wonder why there is such discrepancy in compilers behavior and whether the Standard says that this code should trigger an error or not.


1 Note that this is not a duplicate. In the linked question, OP asked about how to remove ambiguity. I ask why the behavior is different with different compilers.

Fanchan answered 23/8, 2018 at 7:43 Comment(5)
I think this is another spin of CWG Issue 2327.Hewie
Could also be CWG Issue 2137Fornix
@PatrickArtner I am not a native English speaker, so the wording might have been poorly chosen. However, I think the question is pretty clear, simply skip the "I wonder" part and begin with "why".Fanchan
It was a defect in the language addressed in P2828. Now both GCC and Clang accept your example without an error: godbolt.org/z/8T3xe11nrCrossland
MSVC bug report: developercommunity.visualstudio.com/t/…Crossland
K
0

Under the current wording of the standard, this program is ill-formed.

To direct-initialize an object of type X from a prvalue of type Y, constructors of X are considered ([dcl.init.general]/16.6.2, [over.match.ctor]). The viable functions are:

  • X::X(int)
  • X::X(const X&) (implicitly declared)
  • X::X(X&&) (implicitly declared)

All three require a user-defined conversion to initialize their parameter from an argument of type Y.

The Y->int conversion is indistinguishable from the conversions that use Y::operator X() ([over.ics.rank]/3), leading to an ambiguity.


The implementation divergence in the treatment of this example is related to a long-standing language defect identified in CWG2327:

struct Cat {};
struct Dog { operator Cat(); };

Dog d;
Cat c(d);

As described in the issue, initialization of c under the existing rules uses Dog::operator Cat() to materialize a temporary Cat object, from which c is then move-constructed. In contrast, neither Cat c = d; nor Cat c(d.operator Cat()) introduce a temporary ([dcl.init.general]/16.6.3, [dcl.init.general]/16.6.1).

This behavior is undesirable, and no mainstream implementation actually follows the wording of the standard here, opting instead to elide the unnecessary move constructor call in the Cat c(d); case as well. However, the exact strategies for achieving this vary between implementations, leading to inconsistent results in dealing with examples such as yours.

There is an active proposal to fix this bug in the standard, P2828. The paper contains an analysis of current implementation practice, explaining why GCC/Clang accept the example from the OP while MSVC does not. The proposed changes to the standard do not affect the handling of this particular case, leaving it ill-formed.

Karlis answered 13/4 at 17:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.