cplusplus / draft

C++ standards drafts
http://www.open-std.org/jtc1/sc22/wg21/
5.68k stars 749 forks source link

[over.match.funcs.general] p5 user-defined conversions sequence are not considered for object parameter CWG2557 #5364

Open xmh0511 opened 2 years ago

xmh0511 commented 2 years ago

[over.match.funcs.general] p5 says

The implicit object parameter, however, retains its identity since no user-defined conversions can be applied to achieve a type match with it.

It sounds like a note. The implicit conversion sequence only considered whether converting an argument to the type of the parameter can form an implicit conversion sequence. So, "retains its identity" is not matter with the reason why user-defined conversions cannot be applied. Consider this example:

struct A{
   void show(){}
   static void fun(){}
};
struct B{
    using type = A;
    operator A(){
      return A{};
    }
};
B bobj;
bobj.type::show();  // non-static member function  // #1
bobj.type::fun();  // static member function  #2

Even though [class.mfct.non.static] p2 says

If a non-static member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined.

However, it restricts nothing on the conversion sequence that converts the implicit object argument to the type of the object parameter.

It is even worse the status quo is not clear for a static member function. [over.match.funcs.general] p4 says

For static member functions, the implicit object parameter is considered to match any object (since if the function is selected, the object is discarded).

which bullet can interpret why #2 is ill-formed? neither [basic.lookup], [expr.ref], nor [over.match.funcs.general]

I think we should have a formal rule in a similar manner as [over.best.ics.general] p4, in that subclause, it is a reasonable place to talk about conversion sequence.

Such as

User-defined conversion sequences are not considered if the target is the (implicit) object parameter of a member function

[over.match.funcs.general] p5 can refer to the added rule. In addition, the original [over.match.funcs.general] p5 does not cover explicit object parameter, does it mean user-defined conversion sequence can be considered for it?

For the static member case at #2, I haven't found a corresponding rule that can interpret the ill-formed.

jensmaurer commented 2 years ago

I think the example has nothing to do with user-defined conversions or functions. We'd also expect an ill-formed error if bobj.type::show were a (static or non-static) data member. Note that A and B don't derive from each other.

Thus, the right place to look for a restriction is [expr.ref].

jensmaurer commented 2 years ago
struct A {
  static int x;
};

struct B {
  using type = A;
};

int y = B().type::x;

@opensdh, I'm not seeing anything that would make this example ill-formed. I think [expr.ref] should contain a check before the if-ladder on the kind of E2.

xmh0511 commented 2 years ago

Thus, the right place to look for a restriction is [expr.ref].

Yes, that could be a more general way instead of concerning only member functions. If we would have that restriction, is it necessary to remain the sentence(user-defined conversion...) in [over.match.funcs.general] p5?

jensmaurer commented 2 years ago

Yes, I think that's still necessary for cases roughly similar to this:

struct B {
  void operator+(int) const;
};

struct D : B { };

void operator+(const D&, long);

int main()
{
  D() + 0;
}
xmh0511 commented 2 years ago

However, [over.match.oper] p2 will reinterpret the expression to a function call, which depends on what kind of the function is.

For non-member function, it is operator+(D(),0). Instead, for member function, the expression will be reinterpretd to D().operator+(0), D().operator+(0) is till governed by [expr.ref] and only member functions can have a implicit object parameter.

xmh0511 commented 2 years ago

There is also another concern: Are user-defined conversion sequences permitted for an explicit object parameter? This is even not mentioned by [over.match.funcs.general] p5 that only talks about implicit object parameter.

languagelawyer commented 2 years ago

I think the restriction for the non-static case has been mentioned a couple of times in other issues, it is https://eel.is/c++draft/expr.prim.id.general#3 Yes, it is not perfect, but replacing «can» with «shall» and «in which the object expression refers to the member's class» with «in which the object expression is of the member's class … type» feels almost editorial.

For the static members case, I'd say it is implementations' bug. It would be natural to extrapolate CWG315 decision from «we don't care about the result of the object expression» to «also, we don't care about its type» if the RHS of [expr.ref] names a static member.

languagelawyer commented 2 years ago

Plus there is https://eel.is/c++draft/class.access.base#6

jensmaurer commented 2 years ago

CWG2557

xmh0511 commented 2 years ago

@languagelawyer [expr.prim.id.general] p3 and [class.access.base] p6 are well applied to non-static member. I wanted to use the code at #1 to expose some issues about "user-defined conversion sequence", however, it is a wrong way according to the aforemtioned rules.

xmh0511 commented 2 years ago

@jensmaurer If "user-defined conversion sequence restriction" is necessary for implicit object parameter, could we place it in [over.best.ics] and replace it with constructive wording? BTW, how about the explicit object parameter?

jensmaurer commented 2 years ago

I think the explicit object parameter wants all conversions it can get; please double-check the paper that introduced it.

Regarding the move to [over.best.ics]: Yes, we probably could do that. I'm not seeing that high on the priority list, given that it works as currently specified (even though it's not pretty).

opensdh commented 2 years ago

@opensdh, I'm not seeing anything that would make this example ill-formed. I think [expr.ref] should contain a check before the if-ladder on the kind of E2.

Before P1787R6 it did contain

The id-expression shall name a member of the class or of one of its base classes.

but that doesn't work anymore because of using E::e;, so we need a more subtle rule.

jensmaurer commented 2 years ago

@opensdh, I see. I think "E2 shall denote a member of the type of E1." in [expr.ref] will fix it, and will sidestep any using E::e. See CWG2557.

opensdh commented 2 years ago

I don't see how changing "shall name a member" to "shall denote a member" allows

enum E {e};
struct X {
  using E::e;
};
int f(X x) {return x.e;}

because E::e is not a member of X (its declaration in E was simply found by a using-declaration in X).

jensmaurer commented 2 years ago

Thanks for the more complete example. I thought you were concerned about the qualified-id in using E::e itself. Yes, this is a problem. But if we allow this non-member access, maybe we ought to allow the other example, too?

jensmaurer commented 2 years ago

Maybe "If E2 is a qualified-id, the terminal name of its nested-name-specifier shall denote the type of E1 or a base class thereof."

opensdh commented 2 years ago

I think that rule makes sense: if you want to refer to A::x in the example, you can use B::type::x. The permission to find non-non-static members with . does not seem to have any need to be extended to arbitrary qualified-ids.

jensmaurer commented 2 years ago

Amended CWG2557 accordingly.

opensdh commented 2 years ago

Can we strike the insufficient text from [expr.prim.id.general]/3 now? I don't see how an unqualified-id would ever refer to the wrong class.

jensmaurer commented 2 years ago

Good point: CWG2557 is updated.

xmh0511 commented 2 years ago

@jensmaurer @opensdh

enum E {e};
struct X {
  using E::e;
};
int f(X x) {return x.e;}

For this example, only [expr.ref] p6.5 mentions enumerator in a class member access. It says: If E2 is a member enumerator, Is E::e a member enumerator of X? It seems that we consider it is not

because E::e is not a member of X

So, how does [expr.ref] specify that x.e is well-formed? I think [expr.ref] p6.5 might be changed to

If E2 is a ~member~ enumerator and the type of E2 is T, the expression E1.E2 is a prvalue. The type of E1.E2 is T.

Since [basic.lookup.general] p3 says

If any such declaration is a using-declarator whose terminal name ([expr.prim.id.unqual]) is not dependent ([temp.dep.type]), it is replaced by the declarations named by the using-declarator ([namespace.udecl]).


I also think this example is better than the example in https://github.com/cplusplus/draft/issues/4731 to expose the subject of that issue.

jensmaurer commented 2 years ago

It seems we should fix that as a drive-by, plus add the example. CWG2557 is updated.

xmh0511 commented 1 year ago

@jensmaurer @opensdh Another case that would be clarified by the fix is:

struct A{
    enum class C{
        a = 0
    };
};
int main(){
   A a;
   auto c = a.C::a;  //#1
}

For this, I posted the bug file for GCC. However, consider a subtle case

struct A{
    enum C{  // unscoped enumeration results in that `a` is a member of class `A`
        a = 0
    };
};
int main(){
   A a;
   auto c = a.C::a;  //#1
}

Is this well-formed or ill-formed, according to the wording in CWG2557, it is ill-formed since C::a is a qualified-id for which the terminal name of its nested-name-specifier does not denote A. Should the second case deserve to be ill-formed or well-formed?