The following not very long program is treated differently by the current compilers:
class A {
protected:
operator auto();
};
template<class>
struct B : A {
using A::operator auto;
};
B<int> bi;
A::operator auto() { return 1; }
// ok everywhere till this point
B<char> bc; //GCC error here
int main() {
return bc; // MSVC and EDG errors
}
The result of compilation:
'return': cannot convert from 'B<char>' to 'int'. Ambiguous user-defined-conversion
protected function "A::operator int" is not accessible through a "B<char>" pointer or object
error: 'operator auto' has not been declared in 'class A'
Online demo: https://gcc.godbolt.org/z/8cxo3badz
Which implementation is correct here?
First, let's look at what the standard says about the meaning of operator auto
, in case this information is useful later. A function named operator auto
has a declared return type of auto
([class.conv.fct]/2) and its actual return type is deduced from its definition ([dcl.spec.auto.general]/4). This implies that if the definition hasn't been seen yet, then the return type is unknown.
Next, what is the effect of the using-declaration in B<int>
when that specialization is instantiated? [namespace.udecl]/1:
[...] Each using-declarator in a using-declaration names the set of declarations found by lookup ([basic.lookup.qual]) for the using-declarator, except that class and enumeration declarations that would be discarded are merely ignored when checking for ambiguity ([basic.lookup]), conversion function templates with a dependent return type are ignored, and certain functions are hidden as described below. [...]
The set of declarations named by A::operator auto
is the set found by qualified lookup. operator auto
isn't a template, so it isn't ignored, but we do still need to answer the question of whether it's found, in case there's something tricky there. [basic.lookup.qual] tells us to refer to [class.member.lookup], which in turn requires us to know how to do a single search in A
, which is defined by [basic.lookup.general]/3. The lookup for operator auto
in A
will find the declared operator auto
if those two names are considered the same. To determine whether they're the same name, we must refer to [basic.pre]/4.2. This is the first point at which the standard is unclear. In order to determine whether operator auto
(the declared name of A
's conversion function) and operator auto
(the name we are now looking up in order to determine the declaration set named by B
's using-declaration) are the same name, we need to check whether they are "formed with equivalent ([temp.over.link]) types". But [temp.over.link] does not explain when two placeholder types are "equivalent".
I personally can't imagine a legitimate use case for actually looking up operator auto
and am inclined to suggest to CWG that it should be banned, but... anyway, let's move on and assume that the name lookup succeeds (implying that the using-declaration in B<int>
names the declaration set consisting of only the declaration of A
's conversion function), since no compiler seemed to have an issue with the instantiation of B<int>
.
When A::operator auto
is defined, there is a valid argument that the name of operator auto
in A
"changes" to operator int
, since [dcl.spec.auto.general]/1 states:
A placeholder-type-specifier designates a placeholder type that will be replaced later, typically by deduction from an initializer.
And similarly in [dcl.type.auto.deduct]/1
Placeholder type deduction is the process by which a type containing a placeholder type is replaced by a deduced type.
If the auto
in operator auto
is literally replaced by the deduced type, then it could be argued that the conversion-function-id itself changes, since it contains that placeholder type. That being the case, there is some justification for GCC's behavior: in B<char>
the using-declaration doesn't find anything because it's looking for operator auto
in the scope of A
, but only finds operator int
.
I wouldn't say that GCC's behaviour here is wrong, but it's probably not intended. It's easiest to just ban using A::operator auto;
entirely as I pointed out earlier, but let's instead assume that we want to give using A::operator auto;
some sensible semantics. If that were the case, then we would probably take the point of view that the name of A::operator auto
doesn't actually change to operator int
once placeholder type deduction is done. That would mean that the using-declaration in B<char>
has the same effect as the one in B<int>
.
Moving on again, what happens when we attempt to convert a B<char>
lvalue into int
? The overload resolution for this conversion is governed by [over.match.conv], according to which:
[...] The permissible types for non-explicit conversion functions are those that can be converted to type
T
via a standard conversion sequence ([over.ics.scs]). [...]
Not only int
is a permissible type, but also cv int
, any other possibly cv-qualified arithmetic type (including bool
), and references to the aforementioned types.
Next, we apply [over.match.funcs.general]/7:
In each case where conversion functions of a class
S
are considered for initializing an object or reference of typeT
, the candidate functions include the result of a search for the conversion-function-idoperator T
inS
. [Note 3: This search can find a specialization of a conversion function template ([basic.lookup]). —end note] Each such case also defines sets of permissible types for explicit and non-explicit conversion functions; each (non-template) conversion function that
- is a non-hidden member of
S
,- yields a permissible type, and,
- for the former set, is non-explicit
is also a candidate function. [...]
So we first need to look up operator int
in B<char>
. The effect of the using-declaration on this lookup is that if the using-declarator's name is the same as the name being looked up, then the using-declarator is considered as if it were replaced by the set of declarations it names (that is, the declaration of operator auto
in A
). Again, it's not clear whether operator auto
is the same name as operator int
because [temp.over.link] doesn't explain how to determine whether they are, so let's just assume they are not. So we get no candidate from the first half of [over.match.funcs.general]/7. But we also need to consider conversion functions that are "non-hidden members" of B<char>
and that yield a permissible type.
Presumably, operator auto
that has been deduced to have return type int
is considered to "yield" int
, which is one of the permissible types in this case. But I am not sure what "non-hidden member" means exactly, since the standard doesn't formally define the term "hidden". Moreover, saying "each (non-template) conversion function that [...] is also a candidate function" seems to bypass the name lookup process and find the functions directly rather than their declarations, which makes it unclear whether the using-declaration changing accessibility has any effect at all in this scenario. [class.access.general]/4 states that "when a using-declarator is named" access control is applied to the using-declaration itself and not the declarations that replace it; in this case nothing is being named since name lookup is not done. So, EDG's behaviour makes sense from that point of view: it uses the original declaration of A::operator auto
, which is protected, rather than the using-declaration.
If your intuition tells you that it makes no sense that the using-declaration would not be able to change the effective access level here, then perhaps the Clang developers agreed with you. But the wording would need to be fixed here, e.g., by amending [over.match.funcs.general]/7 so that it does proper name lookup (see below).
I'm not sure what MSVC's error message means; it says it's an ambiguous user-defined conversion, but "ambiguous" normally means there is a choice between two or more equally good candidates, and the error message is not listing any candidates.
The changes I personally would recommend to the standard are:
operator T
when T
contains a placeholder type (meaning a compilation error should occur at the instantiation of B<int>
). I searched GitHub for the regular expression using.*operator auto
and all the results I found were a single Clang unit test file and forks thereof. Same goes for \.operator auto
(i.e. a part of an expression that calls operator auto
on some expression of class type). That is, referring to operator auto
, other than to provide a definition of it when it was previously declared, appears to be extremely arcane and unlikely to be missed.operator auto
actually gets replaced by deduction, and clarify that if a conversion-function-id contains an undeduced placeholder type then its name is not considered to be the same as any name that doesn't contain an undeduced placeholder type. That would make using A::operator int;
valid in B
, but only if the definition of A::operator auto
has been seen. (Otherwise, the lookup wouldn't find anything.)U
, look up operator U
in the scope of S
, ignoring all templates, and add the results of all such lookups to the candidate set. (This would mean that using-declarations would have their usual effect.)