David Hollman David Hollman - 3 months ago 11
C++ Question

Implications of conversion function template argument deduction in C++

I'm having trouble understanding the implications of the conversion function template argument deduction rules in the C++ standard. The standard states that ([temp.deduct.conv] clause 1, §14.8.2.3.1 in N4594):


Template argument deduction is done by comparing the return type of the conversion function template (call it P) with the type that is required as the result of the conversion (call it A; see 8.5, 13.3.1.5, and 13.3.1.6 for the determination of that type) as described in 14.8.2.5.


where 14.8.2.5 ([temp.deduct.type]) is the section that describes general template argument deduction (though the most common case, function call template argument deduction [temp.deduct.call], no longer seems to point there; did it ever?). The next clause is what confuses me, though (clause 2):


If P is a reference type, the type referred to by P is used in place of P for type deduction and for any further references to or transformations of P in the remainder of this section.


To me, this seems to imply that
template <class T> operator T()
and
template <class T> operator T&()
are the same (and specifying both would result in an ambiguity). But that isn't the case in any compiler I've used! For instance:

struct any1 { template <typename T> operator T() { } };

struct any2 { template <typename T> operator T&() { } };

void f1(int) { }
void f2(int&) { }
void f3(int const&) { }

int main() {
f1(any1());
// f2(any1()); compile time error
f3(any1());

f1(any2());
f2(any2());
f3(any2());
}


Live Demo

But if references are ignored,
any1
and
any2
should have the same behavior, right? Clearly they don't, since
f2(any1())
doesn't compile with either gcc or clang, while
f2(any2())
compiles fine with both.

The next clause (clause 3, particularly 3.3) confuses things even further:


If A is not a reference type: [...] If P is a cv-qualified type, the top level cv-qualifiers of P’s type are ignored for type deduction.


This, along with clause 2 about the removal of references, would seem to imply that the following code should not compile because of an ambiguity:

struct any3 {
template <typename T> operator T&() { }
template <typename T> operator T const&() { }
};

void f1(int) { }

int main() {
f1(any3());
}


Live Demo

And yet this works fine with both gcc and clang.

What am I missing?

Edit



I should clarify that the way the clang and gcc compilers handle this is exactly what I would expect from a general (relatively advanced) understanding of C++. Some commenters have asked for clarification on what my confusion is (and, implicitly, why I should care). My confusion here is entirely related to trying to understand the implications of the standard. I need a clear understanding of this because I am submitting a paper with code that relies heavily on this working and on my use of it being standards-compliant.

Answer

The key point you're missing is that overload resolution still has to happen. Template deduction isn't the end of the story. Addressing both of your examples separately:


To me, this seems to imply that template <class T> operator T() and template <class T> operator T&() are the same (and specifying both would result in an ambiguity). But that isn't the case in any compiler I've used!

The text you cite indicates that deduction of T is the same for both conversion operators, this is true. But the operators themselves are not the same. You have to additionally consider the rules for binding to references, which are enumerated in [dcl.init.ref]. The section is too long to concisely copy, but the reason that this is an error

f2(any1()); // error

is the same reason that f2(1) is an error: you can't bind an lvalue reference to non-const to an rvalue. As a result, even having both operators isn't in of itself ambiguous:

struct X {
    template <class T> operator T();   // #1
    template <class T> operator T&();  // #2
};

f1(X{}); // error: ambiguous
f2(X{}); // ok! #1 is not viable, calls #2
f3(X{}); // ok! #2 is preferred (per [dcl.init.ref]/5.1.2)

And yet this works fine with both gcc and clang.

struct any3 {
  template <typename T> operator T&();      // #3
  template <typename T> operator T const&() // #4
};

void f1(int) { }

int main() {
  f1(any3());
}

This is an interesting scenario as far as compilers go, because gcc has a bug here. Both candidates should be valid (gcc doesn't consider #4 valid due to 61663). None of the tiebreakers apply on determining best viable candidate, so in this case we have to fall back to [temp.deduct.partial] to determine which candidate is more specializated... which, in this case, is #4.