Overloading and volatile
V

3

6

How is this expected to work?

struct S {};

void foo(volatile S&);
void foo(S);

int main() {
    volatile S v;
    foo(v);
}

Compilers disagree on it: MSVC accepts the code, while Clang and GCC says the call is ambiguous (demo).

We can prove that only the 1st candidate can accept v, if we comment out one of the candidates (and the compilers agree in this case):

  • If we keep only the 1st one, the code compiles.
  • If we keep only the 2nd one, we get an error (S doesn't have a copy ctor. from volatile S&).

EDIT: Since the question was posted, people came up with a simpler example without volatile.

Voodooism answered 20/5 at 13:9 Comment(11)
When you talk about a proof that the call isn't ambiguous you mean the experiment with commenting out one overload? Notice that writing const instead and deleting copy constructor of S leads to the same outcome.Believe
Both binding to the reference and lvalue to rvalue conversion have the same rank: timsong-cpp.github.io/cppwp/over.match.best#tab:over.ics.scsInky
@Abstraction: I don't get it. Show me a demo.Voodooism
Here: godbolt.org/z/Wvf8Ka69MBelieve
foo(v) is an equally good match for both foo() overloads, hence the ambiguity. The test of whether the argument can be passed (e.g. foo(S) requires an accessible copy constructor) occurs AFTER a single "best" candidate overload is picked. MSVC appears to be disregarding candidates prematurely. One reason the standard does it that way is to avoid circumstances where adding an overload silently changes which function is called by existing code (such changes could otherwise be surprising, and cause bugs that are hard to find).Arboriculture
@NathanOliver: Where is an lvalue converted to rvalue? I see no rvalues here. 1st candidate binds a regerence, 2nd candidate would call a copy ctor. (not move ctor.), would it exist. Where is the rvalue?Voodooism
When calling pass-by-value (the void foo(S); case), at the callsite of foo(v); a copy is made of the v. That copy is an rvalue, which will be bound to the parameter of the void foo(S);, which happens — in this case — to be unnamed in the declaration).Mcauley
@Eljay: Let's give a complete example, with names. In void foo(S par); and the call S s; foo(s);, par (an lvalue) is copy-constructed from s (also an lvalue). Where is the rvalue? Do you think, s is first copied to a prvalue, then par is move-constructed from it? I think this doesn't happen. Would be too slow, and unnecessary anyway.Voodooism
The rvalue is at the callsite, which will be bound to the parameter of the void foo(S). For most implementations, when you call a function the arguments are pushed onto the stack (or possibly into registers depending on the calling convention), and then the function is invoked. Those arguments are the local parameters of the function. (This is not how the C++ standard describes it, as the standard describes the C++ abstract machine, which does not specify a stack or heap implementation details.) The values at the callsite that are pushed on the stack are unnamed; they're rvalues.Mcauley
@Eljay: I see only, that par is an lvalue, which is not "bound to an rvalue", but copy-constructed from s. And, both are lvalues. Is this reasoning wrong?Voodooism
The argument is passed-by-value at the callsite. At the callsite, the passed-by-value value that is copy-constructed is unnamed, it is a rvalue. The callsite is not aware that the parameter will be named, or left unnamed, by the invoked function.Mcauley
H
2

The wording that we currently have in the standard about implicit conversion sequences is not great, which is probably why you see this implementation divergence.

Consider [over.best.ics.general] paragraph 1 and the first half of paragraph 6:

An implicit conversion sequence is a sequence of conversions used to convert an argument in a function call to the type of the corresponding parameter of the function being called. The sequence of conversions is an implicit conversion as defined in [conv], which means it is governed by the rules for initialization of an object or reference by a single expression ([dcl.init], [dcl.init.ref]).

[...]

When the parameter type is not a reference, the implicit conversion sequence models a copy-initialization of the parameter from the argument expression. The implicit conversion sequence is the one required to convert the argument expression to a prvalue of the type of the parameter.

This suggests that the implicit conversion sequence from v (an lvalue of type volatile S) to the parameter type S is determined by the rules of copy-initialization of an S object from a volatile S lvalue. This is governed by [dcl.init.general]/16.6.2 since the cv-unqualified version of the type of the initializer is the same as the class type of the object being initialized. Because the overload resolution fails (S has no volatile-qualified copy constructor) sub-sub-bullet 3 applies, and the initialization is ill-formed.

However, when [dcl.init] says that the initialization is ill-formed, and it is a hypothetical initialization for the purpose of forming an implicit conversion sequence, it sometimes means that there is no implicit conversion sequence, and it sometimes means the implicit conversion sequence exists but actually using it in a call would be ill-formed, and you sort of just have to know which one it means. (See CWG2525.)

This matters because if there is no implicit conversion sequence, then the other overload must be selected (the one with a parameter type of volatile S&), but if there is an implicit conversion sequence that would be ill-formed if used for a call, then the overload resolution is ambiguous.

The example in the second half of [over.best.ics.general] paragraph 6 hints weakly that the latter interpretation is correct:

A parameter of type A can be initialized from an argument of type const A. The implicit conversion sequence for that case is the identity sequence; it contains no “conversion” from const A to A.

A could theoretically be a class type whose copy constructor has the form A(A&) (rather than A(const A&)) yet the example is claiming that the implicit conversion sequence is always the identity conversion.

Paragraph 7 is also a weak hint:

When the parameter has a class type and the argument expression has the same type, the implicit conversion sequence is an identity conversion. When the parameter has a class type and the argument expression has a derived class type, the implicit conversion sequence is a derived-to-base conversion from the derived class to the base class. A derived-to-base conversion has Conversion rank ([over.ics.scs]).

The first sentence doesn't apply because the parameter type S is not the same as the type of the argument, volatile S. But the second sentence hints that even if e.g. the copy-initialization of the parameter type from the argument type would not be a simple call to the parameter type's copy constructor (but could actually involve other competing constructors, and may be ambiguous) it doesn't affect the formation of the implicit conversion sequence. So we are led to think that the formation of an implicit conversion sequence to Base from cv Derived always succeeds. If that's the case, it ought to be even more the case for a conversion to S from cv S (and perhaps the first sentence should be amended to include the cv-qualified case).

So I think Clang and GCC are doing what the standard means, but it's honestly not that clear.

Hum answered 21/5 at 1:37 Comment(5)
The intention behind this is unclear to me. Why would one design the language this way? I would prefer the MSVC way, and would rather correct the standard, Clang and GCC to match MSVC.Voodooism
@Dr.Gut There are certain situations where it is desired for an implicit conversion sequence to exist even if it would be ill-formed to perform the actual conversion. An obvious example from C++11 and later is the case where the constructor or conversion function is deleted.Hum
@Dr.Gut In the C++03 context, consider the fact that volatile S is so similar to S (in the same way that const S is very similar to S) that there is some logic to the idea that this is always considered an exact match. What if the second overload had a parameter type of int (and suppose there was a conversion function from S to int)? It might be very surprising to call the int overload in that case.Hum
The argument about deleted functions is not valid. We consider (even currently) the deleted definitions after overload resolution is complete. They participate in overload resolution normally, and can win. █ Your C++03 example has probably a mistake (you meant to replace the 1st candidate, not the 2nd), but I get it. Demo. I agree, that both ways there is a surprise, but still argue, the MSVC way is easier to comprehend, predict or teach.Voodooism
@Dr.Gut If a deleted function is needed in order to form an implicit conversion sequence during overload resolution, the overload that needs the deleted function is still considered, and the overall result may end up being ambiguous.Hum
S
5

Consider this note [over.best.ics.general]p2:

[Note 1: [...]. So, although an implicit conversion sequence can be defined for a given argument-parameter pair, the conversion from the argument to the parameter might still be ill-formed in the final analysis. — end note]

From [over.best.ics.general]p7:

When the parameter has a class type and the argument expression has the same type, the implicit conversion sequence is an identity conversion.

We know that the ICS for the first argument of the second overload (void foo(S);) is the identity conversion (even though the modelled copy-initialization isn't possible).

A volatile S lvalue -> volatile S& for the first argument of the other overload is also an identity conversion (written out in [over.ics.ref]p(1.2)).

Since both ICSs are identical, both overloads are viable but neither is the best, making it ambiguous.

Sidon answered 20/5 at 13:32 Comment(6)
How this relates to: eel.is/c++draft/class.copy.ctor#footnote-91 This implies that the reference parameter of the implicitly-declared copy constructor cannot bind to a volatile? Doesn't this mean that S can't be constructed from volatile S and respective conversion is not available?Cheep
@MarekR It cannot bind, but that doesn't imply that the candidate is non-viable in overload resolution. The fact that it can't bind will become relevant only if the candidate is actually chosen. See links in my previous comment as well.Exhume
@Exhume It's not two identity conversions, but they are still of the same rankNoumenon
Nevermind my earlier deleted comments. I think you are correct.Exhume
Table of conversion categories from C++23: Table 19.Voodooism
As long as I remember, there can't be 2 overloads of the same function differing only in reference vs value parameters. That was one of the primary reasons(not the only one) for introduction of rvalue references into C++. Therefore, MSVC seems to be wrong in this case. But since volatile overloads are rare, this must have slept out of their hands. Someone must file a bug report to Microsoft.Lozar
N
2

Clang and GCC are correct, this is an ambiguous call.

Both overloads of f are viable functions for that call, because one involves no conversion, and the other is an lvalue-to-rvalue conversion. Neither is a better conversion, because lvalue-to-rvalue has the same rank as identity.

That there isn't a copy constructor that accepts volatile S & isn't material to overload resolution where there is an exact match in type.

You can see a similar ambiguity with a deleted copy constructor, however this time MSVC agrees with GCC and Clang:

struct S {
    S() {}
    S(const S&) = delete;
};

This case shows you why the overload resolution rules don't require the conversion to be well-formed. When you = delete an overloaded function, you want it to be picked, and then be found to be ill-formed.

Noumenon answered 20/5 at 13:31 Comment(15)
This isn't the best example because whether a function is defined as deleted is ignored during overload resolution (S has a constructor with signature S(const S&), it's just deleted). If you remove the S(const S&) constructor entirely, MSVC still erroneously accepts it: godbolt.org/z/Wede9xP1oSidon
@Sidon right, msvc is in the wrong, that's a case where the wrongness is more obviousNoumenon
@Caleth: Please correct the example, so that people don't have to read Artyer's comment (which could then be deleted). Thank you.Voodooism
@Dr.Gut the example isn't wrong. Artyer is pointing out a different caseNoumenon
Where is the lvalue-to-rvalue conversion supposed to be?Exhume
@Caleth: Since overload resolution is under analysis, the example has to affect overload resolution. Yours doesn't. From Artyer's comment: whether a function is defined as deleted is ignored during overload resolution. It matters only if overload resolution selects the deleted function.Voodooism
@Exhume foo(v), for foo(S)Noumenon
@Dr.Gut You misunderstand. Artyer is saying the deletedness of a function does not influence overload resolution, not that deleted functions are ignored during overload resolution.Noumenon
@Caleth: foo(v) for foo(S) would call a copy ctor., not a move ctor. No rvalues are involved here. What do you mean? || The example has to influence overload resolution, since the reasoning is about overload resolution.Voodooism
@Dr.Gut The copying by copy constructor is the lvalue-to-rvalue conversion. A deleted function participates in overload resolution. A constrained function template who's constraint isn't met is ignored by overload resolution.Noumenon
@Noumenon But there is no rvalue-to-lvalue conversion there in overload resolution, although the effect is similar. As Artyer quoted in his answer, the conversion is considered the identity conversion (and this is regardless of cv-qulifications, as the standard paragraph before the quote clarifies). And in actual initialization itself, there would also be no lvalue-to-rvalue initialization, because eel.is/c++draft/dcl.init#general-16.6.2 states that constructors are immediately considered, not that such a conversion is applied.Exhume
@Exhume Artyer is wrong, it's an lvalue-to-rvalue conversion. The S is a separate object to v, that's the rvalue.Noumenon
If there was such a conversion, it would have unintended effects on e.g. usage in constant expressions, where lvalue-to-rvalue conversions like this might be forbidden. Generally, with the exception of some unions, lvalue-to-rvalue conversions should be possible only on scalar types.Exhume
@Noumenon So should struct A {} a; constexpr A b = a; be ill-formed then for using an lvalue-to-rvalue conversion?Exhume
@Exhume no. This particular lvalue-to-rvalue conversion is ill formed, because v is volatile qualified. In general, class types have lvalue-to-rvalue conversionsNoumenon
H
2

The wording that we currently have in the standard about implicit conversion sequences is not great, which is probably why you see this implementation divergence.

Consider [over.best.ics.general] paragraph 1 and the first half of paragraph 6:

An implicit conversion sequence is a sequence of conversions used to convert an argument in a function call to the type of the corresponding parameter of the function being called. The sequence of conversions is an implicit conversion as defined in [conv], which means it is governed by the rules for initialization of an object or reference by a single expression ([dcl.init], [dcl.init.ref]).

[...]

When the parameter type is not a reference, the implicit conversion sequence models a copy-initialization of the parameter from the argument expression. The implicit conversion sequence is the one required to convert the argument expression to a prvalue of the type of the parameter.

This suggests that the implicit conversion sequence from v (an lvalue of type volatile S) to the parameter type S is determined by the rules of copy-initialization of an S object from a volatile S lvalue. This is governed by [dcl.init.general]/16.6.2 since the cv-unqualified version of the type of the initializer is the same as the class type of the object being initialized. Because the overload resolution fails (S has no volatile-qualified copy constructor) sub-sub-bullet 3 applies, and the initialization is ill-formed.

However, when [dcl.init] says that the initialization is ill-formed, and it is a hypothetical initialization for the purpose of forming an implicit conversion sequence, it sometimes means that there is no implicit conversion sequence, and it sometimes means the implicit conversion sequence exists but actually using it in a call would be ill-formed, and you sort of just have to know which one it means. (See CWG2525.)

This matters because if there is no implicit conversion sequence, then the other overload must be selected (the one with a parameter type of volatile S&), but if there is an implicit conversion sequence that would be ill-formed if used for a call, then the overload resolution is ambiguous.

The example in the second half of [over.best.ics.general] paragraph 6 hints weakly that the latter interpretation is correct:

A parameter of type A can be initialized from an argument of type const A. The implicit conversion sequence for that case is the identity sequence; it contains no “conversion” from const A to A.

A could theoretically be a class type whose copy constructor has the form A(A&) (rather than A(const A&)) yet the example is claiming that the implicit conversion sequence is always the identity conversion.

Paragraph 7 is also a weak hint:

When the parameter has a class type and the argument expression has the same type, the implicit conversion sequence is an identity conversion. When the parameter has a class type and the argument expression has a derived class type, the implicit conversion sequence is a derived-to-base conversion from the derived class to the base class. A derived-to-base conversion has Conversion rank ([over.ics.scs]).

The first sentence doesn't apply because the parameter type S is not the same as the type of the argument, volatile S. But the second sentence hints that even if e.g. the copy-initialization of the parameter type from the argument type would not be a simple call to the parameter type's copy constructor (but could actually involve other competing constructors, and may be ambiguous) it doesn't affect the formation of the implicit conversion sequence. So we are led to think that the formation of an implicit conversion sequence to Base from cv Derived always succeeds. If that's the case, it ought to be even more the case for a conversion to S from cv S (and perhaps the first sentence should be amended to include the cv-qualified case).

So I think Clang and GCC are doing what the standard means, but it's honestly not that clear.

Hum answered 21/5 at 1:37 Comment(5)
The intention behind this is unclear to me. Why would one design the language this way? I would prefer the MSVC way, and would rather correct the standard, Clang and GCC to match MSVC.Voodooism
@Dr.Gut There are certain situations where it is desired for an implicit conversion sequence to exist even if it would be ill-formed to perform the actual conversion. An obvious example from C++11 and later is the case where the constructor or conversion function is deleted.Hum
@Dr.Gut In the C++03 context, consider the fact that volatile S is so similar to S (in the same way that const S is very similar to S) that there is some logic to the idea that this is always considered an exact match. What if the second overload had a parameter type of int (and suppose there was a conversion function from S to int)? It might be very surprising to call the int overload in that case.Hum
The argument about deleted functions is not valid. We consider (even currently) the deleted definitions after overload resolution is complete. They participate in overload resolution normally, and can win. █ Your C++03 example has probably a mistake (you meant to replace the 1st candidate, not the 2nd), but I get it. Demo. I agree, that both ways there is a surprise, but still argue, the MSVC way is easier to comprehend, predict or teach.Voodooism
@Dr.Gut If a deleted function is needed in order to form an implicit conversion sequence during overload resolution, the overload that needs the deleted function is still considered, and the overall result may end up being ambiguous.Hum

© 2022 - 2024 — McMap. All rights reserved.