C++ Logo

std-proposals

Advanced search

Re: Secondary matching of overloaded operator->

From: Cleiton Santoia <cleitonsantoia_at_[hidden]>
Date: Thu, 17 Jun 2021 08:28:11 -0300
Well, what I want ( and probably many of us want ) is a member function
that receives a member pointer as a parameter with a simple form of calling
( like -> or . ), in a way that you have "this" and "member_ptr" in same
code;

Said that, and since I thought this thru binary operator.() here goes my
2cents :

struct X {
    template< typename MemberType >
    auto operator .( MemberType m ) { // m may be a member function or
member data
        // here we have access to "this" and "m";
    };

    int& operator .( int X::*field ) { // an overload to use for X::b or
X::c
         return this->*field; // does not fall into operator.() case
    };

    auto operator.() default;

    template<typename T> int a(T t) { return t; };
    int b;
    int c;
    float d;
} x;

Upon a call to

x.a(10);

1 - The compiler will try to solve x.a by current template/overload rules
and will eventually decide to call "int X::a(int)", ( may need to
instantiate or not, you may have int a(int) overloaded inside X )
2 - After that it should check for overloaded operator.(), template and non
template versions and thru "normal" instantiaton rules (SFINAE/most
vexing). And decides to instantiate operator.( int (X::*) (int) ) then,
call x.operator.( &X::a ). This way it will find the proper instantiation
even among other operator.() and a´s overloads.
2.1 - If you declare any overloaded operator.() and the compiler does not
found any possibility, it will try to fall back" to operator.() default" if
it is declared;
2.1.1 - If there is no default operator. declared, it should throw a
compilation error.
3 - When you don´d provide any overloads, the compiler will create an
implicit default with "current behavior".

2.1, 2.1.1 and 3 are analogous to constructor/default rules.
And yes, I´m aware that item 2 may mess up a "little" with current template
instantiation rules :)

So:

1 - x.a(10); become: ( x.operator.( &X::a ) ) (10); //
instantiated int X::a(int) and operator.( int (X::*)(int) ),*NOT* passing
10 to operator.(), just &X::a
2 - x.b = 10; become : x.operator.( &X::b ) = 10; // call
overloaded operator.( int X::* ) returning an int&, then call built in
operator=(int&, int)
3 - x.d = 1.9; uses default


If you look closer, you will always calls operator.() with a value that is
a constexpr, so technically, it may be implemented as a non-type template
parameter *unary* operator.()

template<int X::* mem> auto& operator.() { return this->*mem; }
x.b become x.operator.<&X::b>() // I´m not completelly sure where the <>
should be here

However, another more closer++ look, you may see that if operator.() is
inherited ( as it should ), one can get the derived class from the type
parameters.
But since funciton deductiong rules are different then class deduction
rules, I´m not so sure that can be achived thru a unary form, so a
binary operator.()
should get those cases:

struct X{
   template<typename T> auto& operator.( int T::*mem ) { return
((T*)this)->*mem; } // T must be a X or derived from X
   ...
};
struct Y : X {
   int h;
};

Y y;
y.h become x.operator.(&Y::h); // the call is binary now

Finally, properties:

struct X {
     template<typename T> struct property { ... };
     template<typename T, typename MemT> auto& operator.(property<MemT> T
::* mem ) { set_value and invalidate widget } // yay properties !
};
struct Z : X{
     property<int> top;
};

x.top = 10 will convert to x.operator.( &Z::top ) = 10

The operator -> may work in a similar form.


BR
Cleiton

Em ter., 15 de jun. de 2021 às 16:29, Steve Thomas via Std-Proposals <
std-proposals_at_[hidden]> escreveu:

> 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
>>
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>

Received on 2021-06-17 06:28:26