这篇文章很好的讲述了指向成员函数的指针怎么用, 后面对性能做了分析很精彩。
A tutorial on a useful yet poorly understood language feature, useful as a cache or to enable a different sort of polymorphism.
Michael D. Crawford
crawford@goingware.com
Copyright © 2002 Michael D. Crawford.
This work is licensed under a Creative Commons Attribution-ShareAlike 2.5 License.
Abstract
Pointers to Member Functions are one of C++'s more rarely used features, and are often not well understood even by experienced developers. This is understandable, as their syntax is necessarily rather clumsy and obscure.
While they do not have wide applicability, sometimes member function pointers are useful to solve certain problems, and when they do apply they are often the perfect choice, both for improved performance and to make the code sensible. They work very well to cache the result of a frequently made decision, and to implement a different sort of polymorphism.
I discuss what member function pointers are, how to declare and use them, and give some examples of problems that they solve very well.
Contents
- Abstract
- Introduction
- Member Function Pointers Are Not Just Simple Addresses
- Caching the Outcome of a Decision
- The Performance of Member Function Pointers
- Details About Using Member Function Pointers
- A Different Sort of Polymorphism
Introduction
I don't have any hard numbers on how frequently member function pointers are used. While Ido see others mention them sometimes in Usenet and mailing list posts, I have yet to find someoneelse use one in code I have worked with, so my impression is that they are not commonly applied.
Member function pointers are important because they provide an efficient way to cache the outcome of a decision over which member function to call. They can save time, and in some cases, provide a design alternative that avoids the need to implement such decision caching through memory allocation. I will return to this further on.
Member function pointers allow one to call one of several of an object's member functions indirectly. Each of the functions whose "address" is stored must share the same signature.
I put "address" in quotes because the information stored in a member function pointer is not simply the memory address of the start of the member function's code; conceptually it is an offset into the list of functions declared by the class, and in the case of virtual functions will include a real offset into the vtbl
, or table of virtual function pointers.
Member function pointers cannot be dereferenced (have their function called) directly by themselves. They must be called on behalf of some object, that then provides the "this" pointer for use by the member functions.
To illustrate how to declare and call a member function pointer, I will start by giving an example ofdeclaring and dereferencing an ordinary pointer to a non-member function. You declare a functionpointer by giving the prototype of a function it can point to, with the name of the function replacedby (*pointerName)
. Regular function pointers share the same syntax between C and C++:
void Foo( int anInt, double aDouble );void Bar(){ void (*funcPtr)( int, double ) = &Foo; (*funcPtr)( 1, 2.0 );}
For regular function pointers, it is optional to use the address-of operator &
when taking the address of a function, but it is required for taking the address of member functions. g++ will compile source that leaves it out, but emits a warning.
To declare a pointer to member function, you give the prototype of a function it can point to, as before, but the name of this function is replaced by a construction that scopes the pointer - you give it the name of the class whose member functions it can point to, as (ClassName::*pointerName)
. Note that a given member function pointer can only point to functions that are members of the class it was declared with. It cannot be applied to an object of a different class even if it has member functions with the same signature.
You dereference a member function pointer by using .*
or ->*
, supplying a reference or pointer to an object on the left, as appropriate, and the function pointer on the right.
Here is a simple example:
class Foo{
public:
double One( long inVal );
double Two( long inVal );
};
void main( int argc, char **argv )
{
double (Foo::*funcPtr)( long ) = &Foo::One;
Foo aFoo;
double result =(aFoo.*funcPtr)( 2 );
return 0;
}
Declaring a member function pointer is clumsy at best and is hard to get right until you have used them for a while. Rather than declaring them using the full prototype each time, it is helpful to use a typedef
as I show in the example below.
Member Function Pointers Are Not Just Simple Addresses
Most C and C++ programmers know that it is bad style to assume that a pointer is the same size as an int, although this may often be the case. What is less well known is that pointers of different types may not be the same size as each other. For example, in 16-bit x86 programming near pointers and far pointers may have different sizes, where the far pointers consist of the segment and offset together, while near pointers just have the offset. Member function pointers are generally small structures, that encode information about a function's virtualness, multiple inheritance and so on.
In the case of the example shown below, compiled with g++ 2.95.2 on a PowerPC G3 Mac OS X iBook, I found that the size of the member function pointer I created was eight bytes.
This can result in surprises to the user. For example, Microsoft Visual C++ 6 allows the programmer to make an optimization (which is apparently enabled by default) which can cause member function pointers that are intended to be the same type but are declared in different circumstances to have different sizes. Using the wrong setting for your project may result in an apparently gross code generation bug, because a member function pointer returned by a function that supplies them may have a different size than the recipient function expects, causing bogus data to be overwritten on the stack.
There is an item in VC++'s settings labeled "representation" that has a choice between "best case always" and "most general always". If you work with member function pointers in Visual C++, check the documentation for what these settings do and select the right one; if in doubt, select "most general always".
Caching the Outcome of a Decision
One of the best uses for member function pointers is caching the outcome of a decision over which ofseveral member functions should be called in a particular circumstance. If a decision is always going to yield the same result, then it may be faster and even cleaner to make the decision just once ahead of time, then store the outcome in the form of a member function pointer. This is especially advantageous when the decision will be made repeatedly in a loop.
Here is an admittedly silly (but hopefully clear) example, that shows a member function pointer being used to store the outcome of a decision. It also illustrates the use of typedef
:
#include
#include
class Test{
public:
Test( long inVal )
: mVal( inVal )
{}
long TimesOne() const;
long TimesTwo() const;
long TimesThree() const;
private:
long mVal;
};
typedef long (Test::*Multiplier)() const;
int main( int argc, char **argv )
{
using std::cerr;
using std::endl;
using std::cout;
if ( argc != 3 ){
cerr << "Usage: PtrTest value factor" << endl;
return 1;
}
Multiplier funcPtr;
switch( atol( argv[ 2 ] ) ){
case 1:
funcPtr = &Test::TimesOne;
break;
case 2:
funcPtr = &Test::TimesTwo;
break;
case 3:
funcPtr = &Test::TimesThree;
break;
default:
cerr << "PtrTest: factor must range from 1 to 3" << endl;
return 1;
}
cout << "sizeof( funcPtr )=" << sizeof( funcPtr ) << endl;
Test myTest( atol( argv[ 1 ] ) );
cout << "result=" << (myTest.*funcPtr)() <return 0;
}
long Test::TimesOne() const{
return mVal;
}
long Test::TimesTwo() const{
return 2 * mVal;
}
long Test::TimesThree() const{
return 3 * mVal;
}
Now I present an example that does not perform as well as it could because performs a switch
decision many times inside a loop, always reaching the same decision. It is a good candidate to refactor by using a pointer to member function. Again it is a silly example but I wanted to be very clear:
#includeclass Test{
public:
Test( long inFactor )
: mFactor( inFactor ){}
long TimesOne( long inToMultiply ) const;
long TimesTwo( long inToMultiply ) const;
long TimesThree( long inToMultiply ) const;
long MultiplyIt( long inToMultiply ) const;
private:
long mFactor;};
long Test::MultiplyIt( long inToMultiply ) const{
switch( mFactor ){
// decision made repeatedly that always yields the same result
case 1:
return TimesOne( inToMultiply );
break;
case 2:
return TimesTwo( inToMultiply );
break;
case 3:
return TimesThree( inToMultiply );
break;
default:
throw std::exception();
}
}
void MultiplyThem( long inFactor )
{
Test myTest( 2 );
long product; // Call a function that makes the same decision many times
for ( long i = 0; i < 1000000; ++i )
product = myTest.MultiplyIt( i );
}
In most cases where an identical decision is made inside a loop, it is better to refactor the code so that thedecision is outside the loop, and the loop is repeated in each branch of the loop (or packaged inside a subroutine):
void Foo( long value ){ for ( long i = 0; i < 1000000; ++i ){
switch( value ){
// BAD CODE: always reaches the same decision
case 1: //...
break; case 2:
//... break;
case 3: //...
break; }
}
}
Instead we place the switch outside the loop:
void Foo( long value ){ switch( value ){
// BETTER CODE: decision made only once
case 1:
for ( long i = 0; i < 1000000; ++i ){
//...
}
break;
case 2:
for ( long i = 0; i < 1000000; ++i ){
//...
}
break;
//...
}
}
If you want to avoid repeating the loop implementations and each branch of the decision has similar code, you can place them inside subroutines.
Member function pointers are the best solution when it is not practical to refactor this way. Onereason might be that the loop and the decision are in code that belongs to different classes, and you do not want to expose the implementation of the class that makes the decision. Here is the MultiplyIt
code above, refactored to use a pointer to member function:
#includeclass Test{ public: Test( long inFactor ); long TimesOne( long inToMultiply ) const; long TimesTwo( long inToMultiply ) const; long TimesThree( long inToMultiply ) const; long MultiplyIt( long inToMultiply ) const; private: typedef long (Test::*Multiplier)( long inToMultiply ) const; long mFactor; Multiplier mMultFuncPtr; static Multiplier GetFunctionPointer( long inFactor );};Test::Test( long inFactor ) : mFactor( inFactor ), mMultFuncPtr( GetFunctionPointer( mFactor ) ){ return;}Test::Multiplier Test::GetFunctionPointer( long inFactor ){ switch ( inFactor ){ // Decision only made once! case 1: return &Test::TimesOne; break; case 2: return &Test::TimesTwo; break; case 3: return &Test::TimesThree; break; default: throw std::exception(); }} long Test::MultiplyIt( long inToMultiply ) const{ // Using cached decision result return (this->*mMultFuncPtr)( inToMultiply ); }void MultiplyThem( long inFactor ){ Test myTest( 2 ); long product; for ( long i = 0; i < product =" myTest.MultiplyIt(">
The Performance of Member Function Pointers
Unfortunately, calling a member function by dereferencing a member function is more complicated than simply doing a subroutine jump off a register. The pointers are actually small structures and a little bit of work is required to find the actual address of the subroutine to jump to.
I'm afraid I do not have the g++ source code at hand or I could show you the implementation. I know that in tracing through calls via member function pointers in Metrowerks CodeWarrior for Windows, I found that a call would run a small piece of assembly code provided by CodeWarrior's library. This is pretty fast code, and will run very fast in a tight loop if it stays in the CPU's L1 cache, but it is not as fast as a simple compare and conditional branch.
If the decision your code is making repeatedly is very quick to run, it may not be to your advantage to use a member function pointer. A simple if
statement that compares two numeric values, or checks the value of a bool
, or possibly a switch
statement whose alternatives are all contained in a small range (so it is easy for the compiler to build a jump table) may be quicker than dereferencing a member function pointer.
However, if the decision is complicated or lengthy to arrive at, like string comparison or searching some data structure, then using a pointer to member function may be a big win.
Details About Using Member Function Pointers
You may understand the reasons for implementing pointers to member functions as structures if you see that they can be assigned to the addresses of routines with different kinds of implementations, as long as they have the same calling convention:
class Different{ public: inline void InlineMember(); virtual void VirtualMember(); void OrdinaryMember(); static void StaticMember(); typedef void (Different::*FuncPtr)();};void Test(){ Different::FuncPtr ptr = &Different::InlineMember; ptr = &Different::VirtualMember; ptr = &Different::OrdinaryMember;}
(You may be surprised to see me creating a pointer to an inline function, but this is perfectly normal. If you do this, the compiler will place a normal subroutine version of the inline's implementation in an object file and give you the address of that, so the function pointer does not really point to an inline function at all.)
However, although a static member function may appear to have the same calling convention, it really does not because it is not passed the this
pointer - this
is passed to your member functions just like any other parameter, but it is not given explicitly in the member function's prototype. You cannot use pointers to member functions to store the address of a static function (use an ordinary, non-member function pointer for that):
void Fails(){
Different::FuncPtr ptr = &Different::StaticMember;}
mike% c++ different.cppdifferent.cpp: In function `void Fails()':different.cpp:24: initialization to `void (Different::*)()' from `void (*)()'
Pointers to virtual member functions work just like calling a virtual member function directly - the type whose member function gets called is the dynamic type of the object it is called on behalf of, not the static type of the member function pointer:
#includeclass Base{ public: virtual void WhoAmI() const; typedef void (Base::*WhoPtr)() const;};class Derived: public Base{ public: virtual void WhoAmI() const;};void Base::WhoAmI() const{ std::cout << "I am the Base" <<>Base::WhoPtr func = &Base::WhoAmI; Base theBase; (theBase.*func)(); Derived theDerived; (theDerived.*func)(); return 0;}
Running the above program yields the following output:
mike% ./virtualI am the BaseI am the Derived
A Different Sort of Polymorphism
Polymorphism in C++ is usually regarded as always implemented in the form of class heirarchies containing virtual member functions.
An object of a derived class can be supplied to create a pointer or reference to what is apparently the base class; a function pointer lookup in the vtbl
is done when calling a virtual member function off a pointer or reference, so that the function called will be based on the dynamic type that the pointer or reference denotes - that is, it will be from the actual type of the object that was allocated, rather than the static type that the base class pointer or reference is declared as.
However, the concept of polymorphism can take a more general meaning than that, and I have seen mailing list postings advocating that it should also include the use of templates that allow source code with identical syntax to be applied to objects of unrelated types. This std::vector
can be regarded as a polymorphic container that is parameterized by the type supplied as a parameter when a vector object is declared.
Pointers to member functions can be used to implement a different kind of polymorphism. In the regular type, we determine which member function ultimately gets called by allocating objects of different types, that are related members in an inheritance tree. This is implemented by having the vptr
that is a hidden member of the object point at the appropriate vtbl
.
In this other form you create objects that are always of the same type, but determine which member function gets called by choosing which member function's address gets assigned to a member function pointer. One interesting advantage is that you can change the behaviour of an object during its lifetime without having to allocate a new one of a different type as you would with the regular sort of inheritance-based polymorphism.
没有评论:
发表评论