Discussion:
[std-proposals] A new stab at getting operator.() functionality: Via cast operators
Bengt Gustafsson
2018-10-19 02:38:27 UTC
Permalink
There have been multiple attempts at being able to write a general Wrapper
class without having to explicitly write forwarding versions of each method
in the Wrapped class.

The obvious idea would be to allow operator.() to be overridden, but so far
this road has been blocked by difficulties in how to handle collisions
between method names in Wrapper and Wrapped.

Another attempt has been to introduce an idea of inheritance by reference
or indirect inheritance. This was shot down for philosophical reasons with
the sentiment that the Wrapper does not represent a "is-a" relationship to
the Wrapped object. Otherwise this has the nice property of inheriting(!)
the rules of method hiding from regular inheritance: Any declaration in the
"subclass" i.e. Wrapper hides the corresponding name in the "base class",
i.e. Wrapped class. This can be overridden by using Wrapped::<name>
notation as for any inheritance.

This post is a third attempt at solving this problem: Redefine the lookup
rules for cast operators. Currently cast operators are attempted to be able
to pass an object as function parameter. So in the above example a Wrapper
object can be passed to a function taking a Wrapped parameter, invoking the
cast operator. However, this does not happen for an object where a member
is to be accessed, so if Wrapped has a method a you can't invoke a on a
Wrapper. This proposal is to change this so that if no method a is found in
Wrapper a search is done in Wrapped also.

Here is a simple example:


struct A {
void a();
};

class B {
public:
operator A&() { return m_a; }

private:
A m_a;
};

void fun(A& a)
{
}

int main()
{
B b;
fun(b); // Works by invoking the cast operator.
b.a(); // Currently an error, but works with this proposal.
}

The problem of a class with multiple cast operators causing possible
ambiguity should be solved according to the same rules as for multiple
inheritance. Disambiguation can however not be performed using the Class::
syntax as we are not inheriting. Instead static_cast<A&>(b).a() would be
the way to explicitly invoke the cast operator and thus tell the compiler
which instance of a named entity to use.

I think the use cases and motivation for this proposal are well established
as it solves the same problems that operator.() was addressing. The
interesting question is whether there are backwards compatibility issues or
confusion created by introducing this feature into the language.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/a195e287-a40b-458c-88be-e281c92e595d%40isocpp.org.
Richard Hodges
2018-10-19 07:57:45 UTC
Permalink
On Fri, 19 Oct 2018 at 03:38, Bengt Gustafsson <
Post by Bengt Gustafsson
There have been multiple attempts at being able to write a general Wrapper
class without having to explicitly write forwarding versions of each method
in the Wrapped class.
Trying to create operator.() seems to me to be treating the symptom rather
than the cause.

Surely what we would all like is a language mechanism that allows us to
semantically indicate that:

"I would like X class to be a forwarding wrapper to Y impl class through Z
kind of 'ownership'"

ownership examples might be (non-exhaustive):

a unque_ptr<>
a shared_ptr<>
a reference_wrapper<>
a contained object (owner<> ?)
a boost::intrusive_ptr<>
a custom os-specific intrusive_ptr (eg when targeting COM, OSX, etc)
and so on.

I seem to remember seeing some work on metaclasses for c++, which would
completely solve this issue, as well as many others.

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0707r3.pdf
Post by Bengt Gustafsson
The obvious idea would be to allow operator.() to be overridden, but so
far this road has been blocked by difficulties in how to handle collisions
between method names in Wrapper and Wrapped.
Another attempt has been to introduce an idea of inheritance by reference
or indirect inheritance. This was shot down for philosophical reasons with
the sentiment that the Wrapper does not represent a "is-a" relationship to
the Wrapped object. Otherwise this has the nice property of inheriting(!)
the rules of method hiding from regular inheritance: Any declaration in the
"subclass" i.e. Wrapper hides the corresponding name in the "base class",
i.e. Wrapped class. This can be overridden by using Wrapped::<name>
notation as for any inheritance.
This post is a third attempt at solving this problem: Redefine the lookup
rules for cast operators. Currently cast operators are attempted to be able
to pass an object as function parameter. So in the above example a Wrapper
object can be passed to a function taking a Wrapped parameter, invoking the
cast operator. However, this does not happen for an object where a member
is to be accessed, so if Wrapped has a method a you can't invoke a on a
Wrapper. This proposal is to change this so that if no method a is found in
Wrapper a search is done in Wrapped also.
struct A {
void a();
};
class B {
operator A&() { return m_a; }
A m_a;
};
void fun(A& a)
{
}
int main()
{
B b;
fun(b); // Works by invoking the cast operator.
b.a(); // Currently an error, but works with this proposal.
}
The problem of a class with multiple cast operators causing possible
ambiguity should be solved according to the same rules as for multiple
syntax as we are not inheriting. Instead static_cast<A&>(b).a() would be
the way to explicitly invoke the cast operator and thus tell the compiler
which instance of a named entity to use.
I think the use cases and motivation for this proposal are well
established as it solves the same problems that operator.() was addressing.
The interesting question is whether there are backwards compatibility
issues or confusion created by introducing this feature into the language.
--
You received this message because you are subscribed to the Google Groups
"ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an
To view this discussion on the web visit
https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/a195e287-a40b-458c-88be-e281c92e595d%40isocpp.org
<https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/a195e287-a40b-458c-88be-e281c92e595d%40isocpp.org?utm_medium=email&utm_source=footer>
.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CALvx3hZJED%3Dhu_j0zQq6BQTo_e3MrpX6b7H5BKHi8L5ri-Rsxw%40mail.gmail.com.
Bengt Gustafsson
2018-10-19 14:35:38 UTC
Permalink
My proposed feature would give us all that you long for. For instance my B
class could just as well have been spelled:

class B {
operator A& { return *ap; }

std::shared_ptr<A> ap;
};

To get a generic wrapper with shared semantics. I think this comes much
more natural than indirect inheritance, where you still need additional
syntax to indicate how the wrapped object is to be conjured up. In this
proposal we only add a new way of finding members in case there is no
member of the specified name in the Wrapper class (B above).
Post by Richard Hodges
Post by Bengt Gustafsson
There have been multiple attempts at being able to write a general
Wrapper class without having to explicitly write forwarding versions of
each method in the Wrapped class.
Trying to create operator.() seems to me to be treating the symptom
rather than the cause.
Surely what we would all like is a language mechanism that allows us to
"I would like X class to be a forwarding wrapper to Y impl class through Z
kind of 'ownership'"
a unque_ptr<>
a shared_ptr<>
a reference_wrapper<>
a contained object (owner<> ?)
a boost::intrusive_ptr<>
a custom os-specific intrusive_ptr (eg when targeting COM, OSX, etc)
and so on.
I seem to remember seeing some work on metaclasses for c++, which would
completely solve this issue, as well as many others.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0707r3.pdf
Post by Bengt Gustafsson
The obvious idea would be to allow operator.() to be overridden, but so
far this road has been blocked by difficulties in how to handle collisions
between method names in Wrapper and Wrapped.
Another attempt has been to introduce an idea of inheritance by reference
or indirect inheritance. This was shot down for philosophical reasons with
the sentiment that the Wrapper does not represent a "is-a" relationship to
the Wrapped object. Otherwise this has the nice property of inheriting(!)
the rules of method hiding from regular inheritance: Any declaration in the
"subclass" i.e. Wrapper hides the corresponding name in the "base class",
i.e. Wrapped class. This can be overridden by using Wrapped::<name>
notation as for any inheritance.
This post is a third attempt at solving this problem: Redefine the lookup
rules for cast operators. Currently cast operators are attempted to be able
to pass an object as function parameter. So in the above example a Wrapper
object can be passed to a function taking a Wrapped parameter, invoking the
cast operator. However, this does not happen for an object where a member
is to be accessed, so if Wrapped has a method a you can't invoke a on a
Wrapper. This proposal is to change this so that if no method a is found in
Wrapper a search is done in Wrapped also.
struct A {
void a();
};
class B {
operator A&() { return m_a; }
A m_a;
};
void fun(A& a)
{
}
int main()
{
B b;
fun(b); // Works by invoking the cast operator.
b.a(); // Currently an error, but works with this proposal.
}
The problem of a class with multiple cast operators causing possible
ambiguity should be solved according to the same rules as for multiple
syntax as we are not inheriting. Instead static_cast<A&>(b).a() would be
the way to explicitly invoke the cast operator and thus tell the compiler
which instance of a named entity to use.
I think the use cases and motivation for this proposal are well
established as it solves the same problems that operator.() was addressing.
The interesting question is whether there are backwards compatibility
issues or confusion created by introducing this feature into the language.
--
You received this message because you are subscribed to the Google Groups
"ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an
To view this discussion on the web visit
https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/a195e287-a40b-458c-88be-e281c92e595d%40isocpp.org
<https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/a195e287-a40b-458c-88be-e281c92e595d%40isocpp.org?utm_medium=email&utm_source=footer>
.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/16b31301-d66c-4bd4-b65a-f2cf98a9697b%40isocpp.org.
m***@gmail.com
2018-10-19 08:39:56 UTC
Permalink
Post by Bengt Gustafsson
There have been multiple attempts at being able to write a general Wrapper
class without having to explicitly write forwarding versions of each method
in the Wrapped class.
The obvious idea would be to allow operator.() to be overridden, but so
far this road has been blocked by difficulties in how to handle collisions
between method names in Wrapper and Wrapped.
Another attempt has been to introduce an idea of inheritance by reference
or indirect inheritance. This was shot down for philosophical reasons with
the sentiment that the Wrapper does not represent a "is-a" relationship to
the Wrapped object. Otherwise this has the nice property of inheriting(!)
the rules of method hiding from regular inheritance: Any declaration in the
"subclass" i.e. Wrapper hides the corresponding name in the "base class",
i.e. Wrapped class. This can be overridden by using Wrapped::<name>
notation as for any inheritance.
This post is a third attempt at solving this problem: Redefine the lookup
rules for cast operators. Currently cast operators are attempted to be able
to pass an object as function parameter. So in the above example a Wrapper
object can be passed to a function taking a Wrapped parameter, invoking the
cast operator. However, this does not happen for an object where a member
is to be accessed, so if Wrapped has a method a you can't invoke a on a
Wrapper. This proposal is to change this so that if no method a is found in
Wrapper a search is done in Wrapped also.
struct A {
void a();
};
class B {
operator A&() { return m_a; }
A m_a;
};
void fun(A& a)
{
}
int main()
{
B b;
fun(b); // Works by invoking the cast operator.
b.a(); // Currently an error, but works with this proposal.
}
Something tells me this was evaluated back in the day and rejected for some
reason. Someone should remind us why.

I am afraid it might hit the same philosophical problems "of the is-a kind"
- essentially this proposal is even more bold then the one with `using`
inheritance.



As a shameless plug
<https://groups.google.com/a/isocpp.org/d/msg/std-proposals/vZti2htmrlQ/IPbrJXR8AgAJ>,
consider

struct A {
void a();
};

class B {
public:
operator A&() { return m_a; }

private:
A m_a;
};

void fun(A& a)
{
}

using A::a; //< import as free function

class B using
{
a;
}

int main()
{
B b;
fun(b); // Works by invoking the cast operator.
b.a(); //< works
a(b); //< also works
}

I am not saying this solves all needs for "operator()." (in quotes) - of
which I am a fan of, but if the issue is just the fact "conversion works
only for params",
then we could hack our way by "just" allowing function params to be passed
using member access syntax, which is essentially UCS.

We could streamline further by not needing to import as free function first
if the class has a conversion operator to the class he is importing a
function from.

class B using
{
A::a; //< allowed, B can pretend to be A
}
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/4c62ab82-216f-4719-9d35-e2abc5db987d%40isocpp.org.
Bengt Gustafsson
2018-10-19 14:51:30 UTC
Permalink
Post by m***@gmail.com
Something tells me this was evaluated back in the day and rejected for
some reason. Someone should remind us why.
I have no recollection of this. But as the discussion around this feature
reach so far back I would be surprised if this was the first time the idea
came up.
Post by m***@gmail.com
I am afraid it might hit the same philosophical problems "of the is-a
kind" - essentially this proposal is even more bold then the one with
`using` inheritance.
I don't see why. A cast operator does not indicate inheritance, so no is-a
promises are being made, indeed a cast operator is most easily thought of
as providing a way to unwrap something wrapped.

An alternate view on all of this would be that we remove the special case
of the LHS expression of the (built in) operator.() and operator-> does not
check for cast operators, in contrast with all other binary operators. I
was uncertain so I tried that this works:

int operator*(const A& rhs) { return 3; }

B b;
*b; // Calls operator above after calling cast operator.

(https://godbolt.org/z/BN77df)

I don't entirely follow your shamelessly plugged code, but you mention UCS
(unified call syntax), which of course makes this case fairly odd, unless
this proposal is accepted:

a(b); // May work via UCS in case it is acceptable to first call a cast
operator on b before calling its method a. This is unclear and may be one
of the contentious points with UCS.
b.a(); // Does not work with UCS unless this proposal is accepted.

void c(A& a);

c(b); // Works as always, by using the cast operator from B to A&
b.c(); // Probably works with UCS only.

My comments on this are that with UCS and not this proposal we get into a
strange land where it is unclear which of the four calls above are valid.
With both UCS and this proposal all would surely be valid. Saying that this
is not needed if we get UCS is not true, and apart from that UCS has also
been rejected so far.
Post by m***@gmail.com
As a shameless plug
<https://groups.google.com/a/isocpp.org/d/msg/std-proposals/vZti2htmrlQ/IPbrJXR8AgAJ>,
consider
struct A {
void a();
};
class B {
operator A&() { return m_a; }
A m_a;
};
void fun(A& a)
{
}
using A::a; //< import as free function
class B using
{
a;
}
int main()
{
B b;
fun(b); // Works by invoking the cast operator.
b.a(); //< works
a(b); //< also works
}
I am not saying this solves all needs for "operator()." (in quotes) - of
which I am a fan of, but if the issue is just the fact "conversion works
only for params",
then we could hack our way by "just" allowing function params to be passed
using member access syntax, which is essentially UCS.
We could streamline further by not needing to import as free function
first if the class has a conversion operator to the class he is importing a
function from.
I don't understand what you intend the A::a; syntax below to mean.
class B using
{
A::a; //< allowed, B can pretend to be A
}
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/410f61f2-ce98-4714-a81d-199b26bfca89%40isocpp.org.
Barry Revzin
2018-10-19 14:55:46 UTC
Permalink
Post by Bengt Gustafsson
This post is a third attempt at solving this problem: Redefine the lookup
rules for cast operators. Currently cast operators are attempted to be able
to pass an object as function parameter. So in the above example a Wrapper
object can be passed to a function taking a Wrapped parameter, invoking the
cast operator. However, this does not happen for an object where a member
is to be accessed, so if Wrapped has a method a you can't invoke a on a
Wrapper. This proposal is to change this so that if no method a is found in
Wrapper a search is done in Wrapped also.
This seems like it's exactly Bjarne and Gaby's operator.() proposal, except
you're spelling it operator A&() instead of A& operator.()
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/4dc2a29b-2624-4ebd-a8ba-bb82a918aa75%40isocpp.org.
Bengt Gustafsson
2018-10-19 15:22:23 UTC
Permalink
One difference may be that this proposal more naturally extends to ptr->x
which was one of the problems with operator.(). As we can already overload
operator-> we can't use that to overload it in this new way (well we could,
but the effect would be to not be able to reach Wrapper methods via a
Wrapper*). With this proposal it is the searching rules for members that
are changed, no new operators are involved, so these rules would naturally
work equally for . and ->

This proposal also handles multiple cast operators, by using an "as if"
rule for the lookup: As-if the Wrapper inherited from all of them (after
removing references of course). To avoid backwards compatibility issues the
cast operators would only be tried if the member name was not found in any
of the real base classes of the Wrapper. While this rule may seem less than
optimal it seems the only backwards compatible path forward: Only if the
current rules result in a lookup error are the cast operators considered.
Post by Barry Revzin
Post by Bengt Gustafsson
This post is a third attempt at solving this problem: Redefine the lookup
rules for cast operators. Currently cast operators are attempted to be able
to pass an object as function parameter. So in the above example a Wrapper
object can be passed to a function taking a Wrapped parameter, invoking the
cast operator. However, this does not happen for an object where a member
is to be accessed, so if Wrapped has a method a you can't invoke a on a
Wrapper. This proposal is to change this so that if no method a is found in
Wrapper a search is done in Wrapped also.
This seems like it's exactly Bjarne and Gaby's operator.() proposal,
except you're spelling it operator A&() instead of A& operator.()
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/6793597b-148f-41b8-8f1f-85233c93f755%40isocpp.org.
m***@gmail.com
2018-10-19 16:02:21 UTC
Permalink
Post by Bengt Gustafsson
One difference may be that this proposal more naturally extends to ptr->x
which was one of the problems with operator.(). As we can already overload
operator-> we can't use that to overload it in this new way (well we could,
but the effect would be to not be able to reach Wrapper methods via a
Wrapper*). With this proposal it is the searching rules for members that
are changed, no new operators are involved, so these rules would naturally
work equally for . and ->
This proposal also handles multiple cast operators, by using an "as if"
rule for the lookup: As-if the Wrapper inherited from all of them (after
removing references of course). To avoid backwards compatibility issues the
cast operators would only be tried if the member name was not found in any
of the real base classes of the Wrapper. While this rule may seem less than
optimal it seems the only backwards compatible path forward: Only if the
current rules result in a lookup error are the cast operators considered.
Looks to me, this will result to the same issue that UCS faced, in
particular adding methods to the wrapper - it can change behavior of
wrapped objects.

struct A
{
int count();
};
template<class T>
struct W
{
operator T&() {return *a;}

int count(); //< may be later, hides the A::count for all users that use
A via W<A>
private:
T* a;
}
Post by Bengt Gustafsson
Post by Barry Revzin
Post by Bengt Gustafsson
This post is a third attempt at solving this problem: Redefine the
lookup rules for cast operators. Currently cast operators are attempted to
be able to pass an object as function parameter. So in the above example a
Wrapper object can be passed to a function taking a Wrapped parameter,
invoking the cast operator. However, this does not happen for an object
where a member is to be accessed, so if Wrapped has a method a you can't
invoke a on a Wrapper. This proposal is to change this so that if no method
a is found in Wrapper a search is done in Wrapped also.
This seems like it's exactly Bjarne and Gaby's operator.() proposal,
except you're spelling it operator A&() instead of A& operator.()
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/557a5e50-ba1a-4ef0-94c4-5c9cc056148d%40isocpp.org.
Loading...