Article View: comp.std.c++
Article #3417Generic Object Callbacks
From: ian (i.) willmott
Date: Thu, 22 Feb 1996 23:30
Date: Thu, 22 Feb 1996 23:30
398 lines
16293 bytes
16293 bytes
Subject: Generic Object Callbacks
In an article entitled "Q: Generic Callbacks -- Object->*func(...)"
(reference <4fti32$p3p@bcarh8ab.bnr.ca>), bcwhite@bnr.ca asks
"Does the C++ standard allow for a generic callback to be specified?
Basically, I'd like to be able to pass an arbitrary object and
function of that object to be called at some later time."
What is needed is the ability to use member functions as callbacks
without any constraint on the type of the object they are invoked on.
This is possible in C++ only by using various implementation-dependent
hacks to escape the type system, because the language does not provide
any way to express such a construct.
This is strange, because the use of callbacks is critical to
event-driven programming for applications such as embedded systems and
graphical interfaces; C++, which aims to support object-oriented
programming, ought to allow member functions as well as ordinary
functions to be the target of a callback. As yet, it doesn't.
I suggest that a new data type needs to be added to the language. We
could call it "pointer-to-bound-member-function". A small example will
clarify the requirement:
int *pi;
int (*pf)(int);
void f1(int i)
{
*pi=i;
(*pf)(i);
}
// somewhere else:
class A {
public:
int x;
int f(int);
};
void f2(A *pa)
{
pi=&pa->x; // this is legal
pf=&pa->f; // this isn't
}
The desired semantics here are perfectly clear. Assuming f2() has been
executed first, the second line in f1() should invoke A::f() on the
object referenced by f2's argument, without having to know anything
about the object type. This is exactly analogous with the integer
pointer pi, which is why I included it.
The reason this isn't legal is because of the way function pointers
are implemented, as a single machine pointer into code space. To make
the example work, a pointer into data space, to identify the object,
would have to be saved as well. It would be a bad idea to burden the
implementation of function pointers with an extra word of storage just
to make this work, so a new type is needed to provide this facility.
The name "pointer to bound member function" is suggested by the error
message Cfront produces when you feed it the code above. Specifically,
the type I am proposing will encapsulate the idea of a member function
callback in a completely typesafe way, and will be trivially easy to
implement. Conceptually, it is a two-element structure consisting of
pointer into data space, identifying an object, and a pointer into
code space, identifying a member function on that object. The static
type of such a variable is the signature (return type and arguments)
of the member functions it is compatible with, just as the type of a
regular function pointer is the signature of the functions it can
reference. The only operations defined for this type are assignment,
equality, and callthrough. Static typechecking is done at the point
where a value of this type is created, and where a call is made
through it.
Notice that this type is already implicit in the language. In the
expression
(pa->f)(17)
what is the type of the subexpression (pa->f) ? The parentheses are
not even necessary; the operator associativity produces the same
binding. This means that C++ contains legal expressions whose type
cannot be expressed in the language. I propose that the language be
extended so that this type can be expressed, variables of this type
can be declared, and the obvious operations on them provided.
This is what is needed:
1 - declaration syntax for objects of this type
2 - expressions such as the one above are values of this type
3 - assignment operator for variables of this type
4 - call through
5 - implicit conversion from zero
6 - implicit conversion from signature-compatible pointers-to-function
7 - equality and inequality operators for values of this type
Declaration Syntax
No new keywords are required. A slight modification of the existing
pointer-to-member-function declarator syntax will suffice.
return-type (class::*pbmf)(arg-decl-list)
declares the identifier pbmf to be a pointer-to-bound-member-function
with the specified signature. "struct" should probably be allowed in
place of "class". Similar syntax will be used to declare pointers and
references to objects of this type, arrays of this type, functions
returning this type, function arguments of this type, etc.
If this syntax is inappropriate for some reason, something similar
could be devised instead.
Values of Type Pointer-to-Bound-Member-Function
Given the declaration of class A above, and the following declarations
A obj,&ref,*ptr;
int (A::*pmf)(int);
the following expressions all have type
"pointer-to-bound-member-function of one int parameter returning int":
obj.f
ref.f
ptr->f
obj.*pmf
ref.*pmf
ptr->*pmf
Anywhere it is possible to call a non-static class member function on
an object, it is possible to compute two things: the entry point of
the actual function to be executed, and the object pointer to be
passed to that function. Computing the function entry point may
involve virtual function table lookup, and in the presence of multiple
inheritance the object pointer passed to the function may not be the
same as the address of the object on which the function was called.
The pair of these values constitute a value of type
pointer-to-bound-member-function. This value can be used immediately
to specify a member function call, or it can be saved for later use.
If this happens, information about the type of the object need not be
preserved, because all typechecking except of the actual parameters
used in the call has already been done.
If an expression of one of the first three forms above refers to a
static member function, the data-pointer component of the resulting
pointer-to-bound-member-function will have the value used by the
implementation to represent a null pointer.
Assignment
Assignment to an lvalue of type pointer-to-bound-member-function is
defined to mean copying of the two components of the value on the
right-hand-side of the assignment expression into the corresponding
elements of the lvalue.
Call Through
If the identifier pbmf denotes a pointer-to-bound-member-function, and
the arguments supplied in an expression of the form
(*pbmf)(arg-list)
match the function signature of pbmf, then the expression specifies a
function call through the value contained in pbmf, the type of the
expression is the return type of pbmf, and the value of the expression
is the value returned by the called function.
If the data-pointer component of the value of pbmf is non-null, it
will be passed to the called function using the usual mechanism for
object-pointers in member function calls; if it is null, it will not
be. In this most implementations, this will mean that it will be
pushed onto the stack after all the other parameters only if it is
non-zero. The code-pointer component of the value of pbmf specifies
the exact entry point of the function to be executed. If it is null,
the resulting behaviour is undefined.
In order to match the syntax for calls through regular
pointers-to-function, the expression
pbmf(arg-list)
should be allowed and have the same meaning.
Implicit Conversions
A value of type pointer-to-function can be implicitly converted to a
pointer-to-bound-member-function of the same signature. The
code-pointer component of the resulting PBMF has the same value as the
pointer-to-function, and the data-pointer component of the PBMF has
whatever value the implementation uses to represent null data
pointers. This implies that there is an implicit conversion to PBMF
from zero, since zero can be implicitly converted to
pointer-to-function.
Equality and Inequality Operators
Two values of the same pointer-to-bound-member-function type may be
compared to each other using the == and != operators. This is defined
to mean component-wise comparison of the two values, which are
considered to be equal only if the corresponding components are both
equal, and not equal otherwise.
General Comments
For reasons of syntactic consistency, declarations such as the
following should probably be allowed:
int class::*pbmd;
Objects declared this way will have the same semantics and
implementation as normal data pointers and there should be implicit
conversions in both directions between them.
PBMFs are upward compatible with regular pointers-to-function;
anywhere a callback might have to refer to member function on an
object, it should be declared as a PBMF; either type can then be
assigned to it, and the callthrough will behave appropriately. Where
this facility is not needed, pointers-to-function will suffice.
The proposed type pointer-to-bound-member-function introduces to C++
no new keywords, only one new piece of syntax, the declarator, and one
new run-time operation, the callthrough, consisting of a conditional
stack push and a function call. It is statically type-checked in
exactly the same way as a regular member function call; it merely
separates member function lookup and invocation. Objects of this type
are opaque; there is no way to access the two components independently
and consequently it is completely type-safe, something that cannot be
said for the kludges currently necessary to implement the
functionality it provides. This datatype can be implemented in two
machine words on linear address-space architectures and requires no
additional run-time support. It appears to be completely orthogonal to
the rest of the C++ language.
Liabilities
I can think of only one possible problem. If a PBMF referring to a
virtual function is exported from a base class constructor invoked for
a derived class object, then when a call through that PBMF is made,
the member function invoked will be the one defined for the base class
and not the derived class. No simple implementation can prevent this
possibility. It's pretty hard to think of any good reason why you
would want to do this. This is analogous to the situation of calling a
pure virtual function from a base class constructor, although at least
that can be detected at runtime. There is the corresponding
possibility of importing a PBMF into a base class destructor, but
that's even more unlikely. Any function pointers used in code called
from constructors and destructors would be much better expressed as
pointers-to-member-function.
Possible Objections
"You can do this with an object pointer and a pointer-to-member-function."
Only if all of your callbacks are to objects in the same class
hierarchy (a severe restriction), and to members declared in exactly
the same class. It's not even good enough that they be declared in
classes derived from a common base type, since if D is derived from B,
you can assign an value of type (B::*)() to an object of type
(D::*)(), but not the other way around. There's a good reason for this
rule: if it didn't exist it would be possible to call a member
function on an object of a class for which it was not defined.
This points out the need for a type which encapsulates an object
reference and a member function reference so that there is no
possibility of applying the function reference to an object of
inappropriate type.
"There are other ways to do this."
There are two that I know of.
class pbmf {
void *ptr;
int (*func)(void *,int)
public:
pbmf(void *p,int (*f)(void *,int)):
ptr(p),func(f) {}
int operator()(int x)
{ return(func(ptr,x)); }
};
int Af(void *p,int x)
{
return(((A *) p)->f(x));
}
A obj;
pbmf pf(&obj,&Af);
pf(17);
This isn't typesafe, it requires you to rewrite the class declaration
for every new function signature, and to write a wrapper function
around every member function you want to use this way, and imposes the
overhead of an extra function call.
class pbmfbase {
public:
virtual int operator()(int);
};
template<class T> class pbmf: public pbmfbase {
T *ptr;
int (T::*func)(int);
public:
pbmf(T *p,int (T::*f)(int))
ptr(p),func(f) {}
int operator()(int x)
{ return(ptr->*func(x)); }
};
pbmfbase *pf=new pbmf<A>(&obj,&A::f);
(*pf)(17);
This requires a new template for every different function signature
(you might be able to do something with member function templates), it
costs an extra virtual function call, and because the callback has to
be done through a pointer or reference, objects of this type are
probably going to end up on the heap, although you might be able to
avoid this with some other type-unsafe tricks. Using templates is
really overkill for an application this simple.
These are really ugly hacks to accomplish something that should be
directly supported by the language. The fact that the type
pointer-to-bound-member-function is implicit but unexpressable in the
language shows that this is a real deficiency.
"Any built-in type should fit in a single processor register."
According to the discussion in the ARM and the D&E, pointers to member
function are represented by structures with at least three elements.
PBMFs are much easier to implement than pointers-to-member-function.
Also, floating point numbers often do not fit into a single register on
machines without hardware floating point support.
"What will happen if a PBMF is called through after the object it
refers to has been destroyed?"
This is the same situation as calling a member function through a
regular pointer which does not reference a valid object, either
because the pointer is invalid, or the object has been destroyed. The
language does not prevent you from doing this.
"It's not type-safe."
PBMFs are statically type-checked in exactly the same way that regular
member function calls are. They simply separate the object
type-checking from function invocation. Once member function lookup
and pointer adjustment are done, there is no further need for object
type information.
I realize that the probability of any more extensions to the standard
being accepted at this point is small, but I thought that it would be
worth discussing this one anyway. I would not be at all surprised to
hear that something similar to this has been proposed before, although
I saw no mention of any such in the D&E. If so, I would like to know
why it was rejected.
I would like to know how many people think that this would be a useful
language feature. It seems to me that it would make object-oriented,
event-driven programming much easier. I have been working on the
embedded control software for a network switch for the last two years
and the need for this facility has arisen over and over again, but I
have had to make do with the sort of kludges listed above. When the
GNU C++ compiler becomes more stable, I will look into adding this
datatype to it. All comments are welcome.
Ian Willmott
Bell-Northern Research
Ottawa, Ontario
(613)-763-9688
willmott@bnr.ca
---
[ To submit articles: Try just posting with your newsreader. If that fails,
use mailto:std-c++@ncar.ucar.edu
FAQ: http://reality.sgi.com/employees/austern_mti/std-c++/faq.html
Policy: http://reality.sgi.com/employees/austern_mti/std-c++/policy.html
Comments? mailto:std-c++-request@ncar.ucar.edu
]
Technical Details
Message-ID:
<pgpmoose.199602221531.14635@isolde.mti.sgi.com>Path:
arc1-ca.pugleaf.net!chronicle.mti.sgi.com!austernShow Full Headers
Complete Article Headers
Path: arc1-ca.pugleaf.net!chronicle.mti.sgi.com!austern From: "ian (i.) willmott" <willmott@bnr.ca> Newsgroups: comp.std.c++ Subject: Generic Object Callbacks Date: 22 Feb 1996 15:30:31 PST Organization: - Approved: austern@isolde.mti.sgi.com Message-ID: <pgpmoose.199602221531.14635@isolde.mti.sgi.com> NNTP-Posting-Host: isolde.mti.sgi.com X-Original-Date: Tue, 20 Feb 1996 01:37:00 -0500 Content-Identifier: Generic Objec... X-Auth: PGPMoose V1.1 PGP comp.std.c++ iQBVAwUBMSz8uky4NqrwXLNJAQHELQH/fmc4KooMLMLXV2O9kZhZhH8xIa+UYwhU oQT0C8j+A2Xi+BeOjTwu6fRmfc0d3NkYKrO7f0vXsaEfEY7Apk74lQ== =zEk4 Originator: austern@isolde.mti.sgi.com