After being confused why my code gave me an ambiguity error on GCC but no errors on Clang, I simplified the code. It can be seen below.
struct Foo
{
// Foo(Foo&&) = delete;
// Foo(const Foo&) = delete;
Foo(int*) {}
};
struct Bar
{
template<typename T>
operator T()
{
return Foo{nullptr};
}
};
int main() { Foo f{Bar{}}; }
The errors are as follows.
main.cpp:17:18: error: call to constructor of 'Foo' is ambiguous
int main() { Foo f{Bar{}}; }
^~~~~~~~
main.cpp:1:8: note: candidate is the implicit move constructor
struct Foo
^
main.cpp:1:8: note: candidate is the implicit copy constructor
main.cpp:5:1: note: candidate constructor
Foo(int*) {}
^
I was unable to make it successfullycompile for Clang this time, so I suppose that was just a Clang bug and this is the intended behavior.
When I explicitly delete the copy and move constructors (i.e. uncomment the top two lines of code), I instead get
note: candidate constructor has been explicitly deleted
but still an error. How would I go about disambiguating the construction here, then?
Note that I specifically added the Foo{nullptr}
instead of just nullptr
, but there is no difference. Same with marking the Foo
ctor explicit. This ambiguity error only occurs when Bar
's conversion operator is templated.
I can add some SFINAE to the conversion operator, but I am unsure of what I would exclude. For example, this would make it work:
template<typename T, std::enable_if_t<std::is_same<T, Foo>{}>* = nullptr>
This is another one that I found and this might be my answer:
template<typename T, std::enable_if_t<!std::is_same<T, int*>{}>* = nullptr>
To resolve the ambiguity, add explicit
to the conversion operator declaration:
struct Bar
{
template<typename T>
explicit operator T()
{
return Foo{nullptr};
}
};
Why is it necessary? Because Foo
has a constructor taking a int*
, and so a operator int*()
instantiation of operator T()
template is being considered as part of overload resolution for the initialization of f
. See under [over.match.copy]:
1 [...] Assuming that “
cv1 T
” is the type of the object being initialized, withT
a class type, the candidate functions are selected as follows:
(1.1) The converting constructors of
T
are candidate functions.(1.2) When the type of the initializer expression is a class type “
cv S
”, the non-explicit conversion functions ofS
and its base classes are considered. When initializing a temporary object ([class.mem]) to be bound to the first parameter of a constructor where the parameter is of type “reference to possibly cv-qualifiedT
” and the constructor is called with a single argument in the context of direct-initialization of an object of type “cv2 T
”, explicit conversion functions are also considered.
From (1.2) it follows that only implicit conversion functions are considered for the initialization, hence the ambiguity -- as the compiler cannot decide between constructing f
using a reference to Foo
or, as already mentioned, using a int*
(obtained by way of copy-initializing) from the return value of operator int*
. However, when the initializer expression is a temporary object, we also consider explicit conversions -- but only if they match a constructor taking a reference to Foo
, our "possibly cv-qualified T
", i.e. our copy and move constructors. This entire behavior is being consistent with [class.conv.fct¶2]:
A conversion function may be explicit ([dcl.fct.spec]), in which case it is only considered as a user-defined conversion for direct-initialization ([dcl.init]). Otherwise, user-defined conversions are not restricted to use in assignments and initializations.
And so, saying the same thing here for the 3rd time: if it isn't marked as explicit
, there's nothing stopping the compiler from trying to copy-initialize an int*
to be used for construction.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With