4

In C++, I'm searching for the crucial sections of the standard explaining the subtle difference in behavior I've observed between the language's two pointer-to-member access operators, .* and ->*.

According to my test program shown below, whilst ->* seems to allow its right-hand expression to be of any type implicitly convertible to pointer to member of S, .* does not so. When compiling with gcc and clang, both compilers yield errors for the line marked '(2)' stating that my class Offset cannot be used as a member pointer.

Test Program https://godbolt.org/z/46nMPvKxE

#include <iostream>

struct S { int m; };

template<typename C, typename M>
struct Offset
{
    M C::* value;
    operator M C::* () { return value; }  // implicit conversion function
};

int main()
{
    S s{42};
    S* ps = &s;
    Offset<S, int> offset{&S::m};

    std::cout << ps->*offset << '\n';  // (1) ok
    std::cout << s.*offset << '\n';    // (2) error
    std::cout.flush();
}

Compiler Output

GCC 12.2:
    'offset' cannot be used as a member pointer, since it is of type 'Offset<S, int>'
clang 15.0:
    right hand operand to .* has non-pointer-to-member type 'Offset<S, int>'

Program Variation

In order to prove that ->* actually performs an implicit conversion using Offset's conversion function in the test program shown above, I declared it explicit for test purposes,

    explicit operator M C::* () { return value; }  // no longer implicit conversion function

resulting in the compilers to also yield errors for the line marked '(1)':

GCC 12.2:
    error: no match for 'operator->*' (operand types are 'S*' and 'Offset<S, int>')
    note: candidate: 'operator->*(S*, int S::*)' (built-in)
    note:   no known conversion for argument 2 from 'Offset<S, int>' to 'int S::*'
clang 15.0:
    error: right hand operand to ->* has non-pointer-to-member type 'Offset<S, int>'

Research

Whilst there is a well-documented difference between the two operators in that ->* is overloadable and .* is not, my code obviously does not make use of this option but rather relies on the built-in operator ->* defined for raw pointer type S*.

Besides differences in overloadability, I merely found documentation stating the similarity of the expressions. Cited from the standard (https://open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4868.pdf):

[7.6.4.2] The binary operator .* binds its second operand, which shall be of type “pointer to member of T” to its first operand, which shall be a glvalue of class T or of a class of which T is an unambiguous and accessible base class. The result is an object or a function of the type specified by the second operand.

[7.6.4.3] [...] The expression E1->E2 is converted into the equivalent form ((E1)).*E2.

And cited from cppreference.com (https://en.cppreference.com/w/cpp/language/operator_member_access#Built-in_pointer-to-member_access_operators):

The second operand of both operators is an expression of type pointer to member ( data or function) of T or pointer to member of an unambiguous and accessible base class B of T. The expression E1->*E2 is exactly equivalent to (*E1).*E2 for built-in types; that is why the following rules address only E1.*E2.

Nowhere have I found a notion of conversion of the right hand operand.

Question

What have I overlooked? Can someone point me to an explanation of this difference in behavior?

ngmr80
  • 135
  • 5
  • What I am curious about is why you would even need a pointer to a member. I've been using C++ for close to 30 years now and never needed it. There is always a better option from a design point of view. – Pepijn Kramer Jan 04 '23 at 18:13
  • @PepijnKramer Use cases of pointers-to-member are certainly rare. I remember applying them when writing wrapper classes for the components of a C library. In order to avoid boilerplate, I wrote a class template capable of wrapping a couple of structurally similar components. The policy classes used in conjunction with the template would describe the internals required for the implementation in the form of type aliases, function pointers, and ... pointers to members. – ngmr80 Jan 04 '23 at 19:37
  • 1
    Interesting puzzle. You probably should have `language-lawyer` tag on this question. Does `->*` have the "recursive" behavior that `->` has? – Eljay Jan 04 '23 at 19:58
  • @Eljay Good point. I added that tag. – ngmr80 Jan 04 '23 at 20:02
  • I suspect the answer is that `(*E1).*E2` for built-in types, and `Offset` is not a built-in type. In conjunction with `->*` participating in overload resolution with user defined operators (like `operator M C::* ()`), but `.*` does not. – Eljay Jan 04 '23 at 20:24
  • That must have been an interesting piece of code. Good to know it was some "library" internal stuff. :) – Pepijn Kramer Jan 05 '23 at 04:02
  • 1
    One of my answers was (ab)using `->*` for function extensions. Not something I recommend, but a fun exercise. https://stackoverflow.com/a/57081233/4641116 – Eljay Jan 05 '23 at 12:04
  • 1
    @Eljay I've been tempted to upvote your linked answer and would merely restrain for the very same reason already explained in your editorial note. As someone killing time with C++ as others might do with a '1000-pieces-black-hell' jigsaw puzzle, I'm reasonably excited. Feel applauded! :) – ngmr80 Jan 05 '23 at 16:42

1 Answers1

4

When overloadable operators are used and at least one operand is of class or enumeration type, overload resolution is performed using a candidate set that includes built-in candidates ([over.match.oper]/3) - for ->* in particular, see [over.built]/9.

In this case, a built-in candidate is selected, so the implicit conversion is applied to the second operand, and then ->* is interpreted as the built-in operator ([over.match.oper]/11).

With .*, there's no overload resolution at all, so no implicit conversion either.

T.C.
  • 133,968
  • 17
  • 288
  • 421