Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

object and pointer-to-member closure: trivial intent, arcane implementation

101 views
Skip to first unread message

via....@googlemail.com

unread,
Feb 28, 2015, 4:00:11 PM2/28/15
to

Apologies if this has been posted before. If there is a similar topic,
please point me to it.

Consider the following.

#include <functional>

struct A {};
struct B {};
struct C {};

void f(std::function<void(A, B, C)> g) { g(A(), B(), C()); }

void g(A, B, C) {}

Now, I can do this:

f(g); // (*)

If I further define

struct D
{
void g(A, B, C) {}
};


I could naively hope that the following trivial modification of (*) would
work:

D d;
f(d.g);

It does not (more on that later). The intent, I believe, is fairly obvious:
I want to pass the closure of d with D::g() to f().

I can achieve the goal via this:

D d;
f([&d](A a, B, C c){d.g(a, b, c);});

I dislike this, however, because it is quite verbose and violates the DRY
principle quite blatantly.

Another possible option is:

D d;
f(std::bind(&D::g, d, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3));

The problem with this other option is that it has taken me a few minutes of
trial, error and intense staring at the docs. And I had to read some more
docs just to understand whether this syntax would be functionally
equivalent to the previous option if g() were declared virtual and D were a
part of some class hierarchy. Let's face it, that thing is way too
complicated given the simplicity of the need. And DRY is still being
violated, because I need to spell out the name D and use three place
holders.

Another problem is that now, having at least two different ways of doing
what I need, I would like to understand the tradeoffs involved. Which I
cannot, because both approaches eventually utilize something that is
expressly left unspecified by the standard.

At which point a sane person would say: if both methods invoke some
compiler magic, then perhaps it is the compiler that should figure out
which magic works better, and so it should magically transform the naive
f(d.g) into the appropriate implementation. Which it cannot do, because
f(d.g) is expressly forbidden by the standard. Quoting draft n3797, 5.2.5.4:

-- Otherwise, if E1.E2 refers to a non-static member function and the type
of E2 is "function of parameter-type-list cv ref-qualifieropt returning T",
then E1.E2 is a prvalue. The expression designates a non-static member
function. The expression can be used only as the left-hand operand of a
member function call (9.3).

[end quote]

So d.g is quite explicitly forbidden unless followed by a function call
operator.

f(&d.g) does not work either, because the d.g is still not followed by a
function call operator, and also because d.g is a prvalue, also specified
by 5.2.5.4, and clause 5.3.1.3 (that specifies the semantics of &) works
with either lvalues or qualified IDs, i.e., D::g, not d.g.

I understand why such use was forbidden prior to C++11: there was no
closure support built into the language. But it exists now. As far as I can
tell, the compiler IN PRINCIPLE, upon encountering d.g (or pd->g, where pd
is a pointer to D) and understanding that g is a non-static member function
(which it has to do anyway, if only to handle the function call operator
that must follow, or emit an error message otherwise) could more or less
mechanically transform that code into the code similar to what I wrote
above (or do something much smarter).

Is there any fundamental reason why this could not be done? I realize that
there are complications related to overloaded and template member
functions; but that same sort of complications is also dealt with when the
address of a non-member function is taken.

Would that contradict the standard in some major way?

Thanks in advance.


--
[ comp.std.c++ is moderated. To submit articles, try posting with your ]
[ newsreader. If that fails, use mailto:std-cpp...@vandevoorde.com ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]

Jakob Bohm

unread,
Mar 26, 2015, 1:10:07 PM3/26/15
to

On 28/02/2015 21:59, via....@googlemail.com wrote:

>
> Apologies if this has been posted before. If there is a similar topic,
> please point me to it.
>
> Consider the following.
>
> #include <functional>
>
> struct A {};
> struct B {};
> struct C {};
>
> void f(std::function<void(A, B, C)> g) { g(A(), B(), C()); }
>
> void g(A, B, C) {}
>
> Now, I can do this:
>
> f(g); // (*)
>
> If I further define
>
> struct D
> {
> void g(A, B, C) {}
> };
>
>
> I could naively hope that the following trivial modification of (*) would
> work:
>
> D d;
> f(d.g);
>
> It does not (more on that later). The intent, I believe, is fairly obviou=
s:
> I want to pass the closure of d with D::g() to f().
>
> I can achieve the goal via this:
>
> D d;
> f([&d](A a, B, C c){d.g(a, b, c);});
>
> I dislike this, however, because it is quite verbose and violates the DRY
> principle quite blatantly.
>
> Another possible option is:
>
> D d;
> f(std::bind(&D::g, d, std::placeholders::_1, std::placeholders::_2,
> std::placeholders::_3));
>
> The problem with this other option is that it has taken me a few minutes =
of
> trial, error and intense staring at the docs. And I had to read some more
> docs just to understand whether this syntax would be functionally
> equivalent to the previous option if g() were declared virtual and D were=
a
> part of some class hierarchy. Let's face it, that thing is way too
> complicated given the simplicity of the need. And DRY is still being
> violated, because I need to spell out the name D and use three place
> holders.
>
> Another problem is that now, having at least two different ways of doing
> what I need, I would like to understand the tradeoffs involved. Which I
> cannot, because both approaches eventually utilize something that is
> expressly left unspecified by the standard.
>
> At which point a sane person would say: if both methods invoke some
> compiler magic, then perhaps it is the compiler that should figure out
> which magic works better, and so it should magically transform the naive
> f(d.g) into the appropriate implementation. Which it cannot do, because
> f(d.g) is expressly forbidden by the standard. Quoting draft n3797,
> 5.2.5.4:
>
> -- Otherwise, if E1.E2 refers to a non-static member function and the typ=
e
> of E2 is "function of parameter-type-list cv ref-qualifieropt returning T=
",
> then E1.E2 is a prvalue. The expression designates a non-static member
> function. The expression can be used only as the left-hand operand of a
> member function call (9.3).
>
> [end quote]
>
> So d.g is quite explicitly forbidden unless followed by a function call
> operator.
>
> f(&d.g) does not work either, because the d.g is still not followed by a
> function call operator, and also because d.g is a prvalue, also specified
> by 5.2.5.4, and clause 5.3.1.3 (that specifies the semantics of &) works
> with either lvalues or qualified IDs, i.e., D::g, not d.g.
>
> I understand why such use was forbidden prior to C++11: there was no
> closure support built into the language. But it exists now. As far as I c=
an
> tell, the compiler IN PRINCIPLE, upon encountering d.g (or pd->g, where p=
d
> is a pointer to D) and understanding that g is a non-static member functi=
on
> (which it has to do anyway, if only to handle the function call operator
> that must follow, or emit an error message otherwise) could more or less
> mechanically transform that code into the code similar to what I wrote
> above (or do something much smarter).
>
> Is there any fundamental reason why this could not be done? I realize tha=
t
> there are complications related to overloaded and template member
> functions; but that same sort of complications is also dealt with when th=
e
> address of a non-member function is taken.
>
> Would that contradict the standard in some major way?
>
> Thanks in advance.
>
>
>
>
One complication I have skipped over below is how to disambiguate any
overloading of the member name D::g.

A key consideration that must never be forgotten in such decisions
is not to over-complicate the simplified mental language model that
language users (humans) need to memorize in order to write correct
programs in the language. This is the (unwritten) simplified model
that is instantiated by the more detailed specification in the language
standard.

A major issue is that the expression (&d.g) is already a valid
expression of the type "pointer to member function of class D with a
certain prototype", raising the tricky question of where in the
expression semantics, this value needs to be contextually changed from
a value type that does not include the actual value of "&d", to one
that does. One way would be to define an implicit conversion from the
(unnamed) type "pointer to closure with object at address p" to
"unbound pointer to member function of class D", then redefine the
expression (&d.g) to always be the closure function pointer, but with a
hard requirement that compilers must optimize away the bind-unbind
sequence within a single expression even in un-optimized debug builds.

A further complication is that sane compilers (even if not required by
the standard) will accept the expressions
(&((reinterpret_cast<D*>(nullptr))->g))
and
(&((reinterpret_cast<D*>(AnyInvalidPtr))->g))
as equivalent to (&D::g), but cannot turn that into a valid closure
referring to the invalid class D objects. (In practice, such
expressions tend to occur inside complex macro or template expansions
that look for properties of members independently of their object).
Specifically, the issue is to establish sane rules as to when the
compiler should omit a diagnostic about binding to an invalid object
pointer, giving the conflicting requirements of pointing directly to
the offending expression in the source and postponing the non-use of
the pointer to a later optimization pass. A very similar issue has in
the past caused at least one major compiler to issue spurious
diagnostics from the implementation of things like offsetof(D,x) in
terms of a null D*.

What I don't know about, but which might exist either in a standard C++
version or in e.g. boost, is why there isn't an operator/function such
as example::bind2(&d, &d.g), which uses template or compiler magic to
determine the types of (*(&d)) and (&d.g), then return a bound function
with the same prototype as d.g (except for the hidden this pointer
argument). ("example::" is obviously a placeholder for the relevant
namespace, such as "std::" or "boost::").

Maybe something like (NOT SYNTAX CHECKED, rough sketch)

namespace example {

template<class C, class R> inline
(R (*)(void)) bind2(C *pObj, R (C::*f)(void)) {
return std::bind(f, *pObj);
}
template<class C, class R, class A1> inline
(R (*)(A1)) bind2(C *pObj, R (C::*f)(A1)) {
return std::bind(f, *pObj, A1);
}
template<class C, class R, class A1, class A2> inline
(R (*)(A1,A2)) bind2(C *pObj, R (C::*f)(A1,A2)) {
return std::bind(f, *pObj, A1, A2);
}
// Etc. for all the argument list shapes, including variadics,
// There shall also be repetitions for cv-qualified objects with
// cv-qualified member function pointers.
// There shall also be repetitions for those argument/return types
// not representable by a "class X" template argument.
// For compilers with prototype-modifying compiler specific
// function attributes, this shall all be repeated for each valid
// combination of such.

} // namespace example



Enjoy

Jakob
--
Jakob Bohm, CIO, Partner, WiseMo A/S. https://www.wisemo.com
Transformervej 29, 2860 S=C3=B8borg, Denmark. Direct +45 31 13 16 10
This public discussion message is non-binding and may contain errors.
WiseMo - Remote Service Management for PCs, Phones and Embedded

via....@googlemail.com

unread,
Mar 30, 2015, 1:30:10 PM3/30/15
to

On Thursday, March 26, 2015 at 6:10:07 PM UTC+1, Jakob Bohm wrote:

[...]

> A major issue is that the expression (&d.g) is already a valid
> expression of the type "pointer to member function of class D with a
> certain prototype"

As indicated in my original message, the expression &d.g, where g is a
non-static member function, is invalid in standard C++14. A pointer to a
non-static member function of class D, in standard C++14, is solely
obtainable via &D::g.

Regardless, the point of my message was not about &d.g in particular, which
seems to be the focus of your message. I spoke of extending the meaning of
d.g when it is not followed by the function call operator; such meaning is
unspecified now and such use is forbidden by the current standard.

Supposing the standard is extended as I proposed, so that d.g generates a
temporary closure object, &d.g will be ill-formed unless further extensions
are made, because the current standard disallows taking addresses of
temporaries.


--
0 new messages