C++ Logo

std-proposals

Advanced search

Re: Secondary matching of overloaded operator->

From: Steve Thomas <steve.p.thomas_at_[hidden]>
Date: Tue, 15 Jun 2021 12:28:14 -0700
Hi Arthur,

Thank you for your response.

The answers to problems #1 and #2 are related to the important detail.
operator-> will keep going until it resolves to a native pointer type, then
binds whatever is on its RHS to something in the scope of that pointer's
type. This mechanism would not remove anything from this, but would change
the final binding to a function call within the current class scope
instead. So the purpose of calling operator->() is to resolve it to the
pointer type that can then be used in the subsequent lookup. If Pointee::*f
has multiple overloads, it uses whichever one the compiler would have
otherwise bound the name to in the default behavior.

Major problem #3: Yeah, I forgot to include the args in the function
signature when typing it out. It would have to be something like:
    template<typename R, typename... Args>
    T operator->(Pointee* p, R (Pointee::*f)(Args...), Args... args) {
      return (p->*f)(std::forward<Args>(args)...);
    }

Major problem #4: The process here is that the compiler resolves the
chained operator-> calls to a native pointer of type Ptr as before, looks
for the Ptr:: scope binding of whatever's on the RHS, then looks for an
overload using that binding in the current class scope. If it finds the
overload in the current class scope, it calls that, otherwise it binds to
whatever it found in Ptr:: scope as per the default. While there would be
nothing to prevent you from allowing this mechanism to similarly overload
member access to return a modified data member reference, it seems less
useful. The point of this is mostly to allow the container class to perform
transformations on the function call.

Minor problem #5: I don't think arrow proxy solves what I want to do (but
I'll look at it harder). As currently specified, operator-> has to
eventually resolve to a pointer type, then accesses a member or member
function of that type. I don't see a way within the current specification
of, e.g., modifying the return type of the function that it resolves to.

> You certainly don't want `x->y()` to behave subtly differently from
`(*x).y()`. So any solution you come up with must work uniformly for both
`operator->` and `operator*`.
This is likely the biggest problem, which may well be unsolvable with the
inability to do anything to change the member access operator.


On Tue, Jun 15, 2021 at 9:35 AM Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
wrote:

> On Tue, Jun 15, 2021 at 12:19 PM Steve Thomas via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
>
>>
>> At the moment, an overloaded operator->() eventually resolves to a
>> pointer of arbitrary type, before calling some method or accessing a data
>> member from the type of that pointer.
>>
>> class SmartPointer {
>> public:
>> Pointee* operator->();
>> };
>> SmartPointer s;
>> s->f(); // Resolves to a Pointee* p, then calls Pointee::*f(p).
>>
>
> Important detail: If the return type of `SmartPointer::operator->()` is
> not a native pointer type, then the compiler will generate a call to
> *that* type's operator->(), and so on forever or until a native pointer
> type is finally reached.
>
> I would like to change this, so that instead of immediately
>> calling Pointee::*f(p), we look for a method in the current class scope and
>> if found, call that instead:
>> class SmartPointer {
>> public:
>> Pointee* operator->();
>>
>> template<typename R, typename... Args>
>> T operator->(Pointee* p, R (Pointee::*f)(Args...));
>> };
>> SmartPointer s;
>> s->f(); // Resolves to s.operator-><Pointee>(s->operator(),
>> &Pointee::f);
>>
>
> Minor problem #1: What's the purpose of calling the zero-argument
> `operator->()` here? Shouldn't this example be more like
> struct SP {
> T *ptr_;
> template<class R, class... As>
> R operator->(R (T::*pm)(As...));
> };
> ?
>
> Major problem #2: What if `Pointee::f` has multiple overloads?
>
> Major major problem #3: Show me the implementation of `operator->` above.
> struct SP {
> T *ptr_;
> template<class R, class... As>
> R operator->(R (T::*pm)(As...)) {
> return (ptr_->*pm)(args...); // wait, where the heck did
> these `args...` appear from? How did we get them?
> }
> };
>
> Major problem #4: What if `Pointee::f` is a data member, or
> pseudo-destructor, or any of the other kinds of things that are allowed to
> appear on the right-hand side of an `->` operator?
>
> Minor problem #5: Couldn't you achieve your stated goal more easily by
> just making your optional's `operator->` return a proxy object?
> Are you familiar with the "arrow proxy" idiom?
> https://quuxplusone.github.io/blog/2019/02/06/arrow-proxy/
> I think you need some kind of proxy no matter what. You certainly don't
> want `x->y()` to behave subtly differently from `(*x).y()`. So any solution
> you come up with must work uniformly for both `operator->` and `operator*`.
>
> I suggest you read about arrow_proxy, then see if it completely solves
> your problem, and if not, then come back with a worked example — a complete
> program, with a unit test that "fails now, but would pass if my fantasy
> feature existed." Show the code you'd write using your fantasy feature that
> would make the test pass. Then, people can either show different ways to
> make it pass within today's language, or perhaps they might say "oh yeah, I
> see how that would be useful."
>
> HTH,
> –Arthur
>

Received on 2021-06-15 14:28:28