cplusplus / CWG

Core Working Group
24 stars 7 forks source link

[basic.compound] Current wording seems to disallow formation of pointer to static member function and explicit object member function without `&` operator #572

Open ranaanoop opened 3 months ago

ranaanoop commented 3 months ago

Full name of submitter: Anoop Rana

Reference (section label): [basic.compound]

Issue description:

I've noticed several issue regarding pointer to member. I'll start by describing one of them here in this issue. This particular issue is about id-expression in [expr.prim.id.general] and the fact that expr.prim.id.general places no restriction on explicit object member function Thus making this program well-formed.

To start with, basic.compound says that:

pointers to non-static class members,32 which identify members of a given type within objects of a given class, [dcl.mptr]. Pointers to data members and pointers to member functions are collectively called pointer-to-member types.

Note that the highlighted sentence 2 above doesn't make distinction between static and non-static members and it just group all members(static as well as non-static) into the category of/called pointer-to-member type.

Now, we move onto expr.unary.op that states:

A pointer to member is only formed when an explicit & is used and its operand is a qualified-id not enclosed in parentheses. .

This means that int (*ptr_static)(int) = C::g; in the below program is ill-formed because we haven't used &. But practically all compilers accept the program and we usually expect it to work because it g is a static member function.

struct C{
   static int g(int);
};

int (*ptr_static)(int) = C::g; //accepted by all compilers  but ill-formed as per current wording

Note that there is also dcl.mptr that says that a pointer to member shall not point to static member of a class:

A pointer to member shall not point to a static member of a class ([class.static]), a member with reference type, or “cv void”.

[Note 1: See also [expr.unary] and [expr.mptr.oper]. The type “pointer to member” is distinct from the type “pointer”, that is, a pointer to member is declared only by the pointer-to-member declarator syntax, and never by the pointer declarator syntax[.](https://eel.is/c++draft/dcl.mptr#5.sentence-2) There is no “reference-to-member” type in C++. — end note]

The important thing to note here is that this [dcl.mptr#4] and the note above seems to imply that ptr is not a pointer to member. Note that this would justify the reason for the working of C::g above because we're not forming a pointer to member so expr.unary.op isn't violated. But basic.compound said the pointer to data member and member functions are collectively called pointer-to-member types.

So for this first problem I suggest that [basic.compound#1.8.sentence-2] be changed to only include non-static members. Note that basic.compound#1.3 already include static members. So there is one more reason to restrict [basic.compound#1.8.sentence-2] to only include non-static members.


This problem is even more evident/highlighted when considering explicit object member function. For example, consider the following example for which we see implementation divergence:

struct C{
   int f(this int);
};

//both of these(#1 and #2) are accepted by msvc but rejected by clang and gcc
decltype(C::f) func;       //#1 well-formed as per current wording
int (*ptr)(int) = C::f;    //#2 ill-formed as per current wording

In this program #1 is well-formed as per current wording in expr.prim.id.general as explained in this thread but #2 seems to be ill-formed because C::f can't be used to form a pointer to member as & is not used as per expr.unary.op. Note that msvc accepts both #1 and #2.

Now as per my understanding, the explicit object member function is supposed to behave like a static member function. So I would expect #2 to work as well even though the current wording makes it ill-formed.

So maybe we also need to exempt explicit object member function from basic.compound#1.8.sentence-2.

Basically, ideally C::f should not behave like the old pointer to implicit object member function(and instead should behave like a pointer T) and we don't have to use & before C::f to get a pointer from it. It should also be able to decay implicitly just like normal functions do. So we might also need to do some modification in [conv.func].

Basically, we want to handle these(shown in above code) three cases more consistently.

Suggested resolution:

Change basic.compound#1.8.sentence-2 to only include non-static data members and implicit object member function. Note basic.compound#1.3 already include static members. That is, [basic.compound#1.8] could be changed to as highlighted below:

pointers to non-static class members,32 which identify members of a given type within objects of a given class, [dcl.mptr]. Pointers to non-static data members and pointers to implicit object member functions are collectively called pointer-to-member types.

Note two changes have been made in the above modified reference.

Additionally, basic.compound#1.3 might be modified to include explicit object member function since static members are already included in basic.compound#1.3. That is, [basic.compound#1.3] could be changed to as highlighted below:

pointers to cv void or objects or functions (including static members of classes and explicit object member function) of a given type, [dcl.ptr];

These changes will make the shown two code snippets in this issue well-formed.

Sidenote

Bug has been submitted for decltype(C::f) func; for gcc.

AlanVoore commented 3 months ago

So if I understand correctly, one of the problems that this issue deals with is that both int (*ptr_static)(int) = C::g; and int (*ptr)(int) = C::f; should be well-formed because both of these uses pointer declarator syntax instead of pointer to member declarator syntax and as per the non-normative note cited in this issue we don't have to use & before either C::f as well as C::g. Meaning these should behave similarly/consistently. But a note is non-normative and normative wording in basic.compound#1.8 puts both static and non-static members to be under pointer-to-member types. So we need to change basic.compound#1.8 to only include non-static data member and implicit object member function so that expr.unary#4 don't apply when forming a pointer to static member function or explicit member function.

Is this correct interpretation of one of the problems raise in this issue?

ranaanoop commented 3 months ago

Is this correct interpretation of one of the problems raise in this issue?

@AlanVoore Yes, exactly.

In addition, this issue also points out that decltype(C::f) func; is already well-formed as per current wording which is the expected behavior because decltype(C::f) func; should behave same as decltype(C::g) func;.

Baiscally, an explicit object member function is supposed to behave like a static member function so they should be consistently handled.

AlanVoore commented 3 months ago

Here is one more demo showing the same problem as discussed in this issue:

struct C
{
  static int g(int);
  int f(this int);
};
template<typename T> void func(T&); 
int main()
{
    func(C::g); //accepted by all compiler & well-formed as per current wording
    func(C::f); //rejected by gcc and clang but well-formed as per current wording. MSVC accepts this 
}

Note func(C::f) is well-formed as per current wording but rejected by both clang and gcc and accepted by only msvc. Intuitionally, I would expected it to be valid as well. Note (&C::f)(44); also compiles with gcc and clang and so it works like a static member function as no class type object was needed/passed here in (&C::f)(44);.

jensmaurer commented 3 months ago

Explicit object member functions cannot be named by a qualified-id unless taking the address or when in a this-transformation context. CWG2902 was amended accordingly. See #573.

Is there any remaining issue here, beyond that?