今天看代码突然遇到成员函数指针的使用,以前没有用过,比较生疏,因此google了一下,发现此文,费劲的翻译了下,方便以后查询
原文链接:http://www.goingware.com/tips/member-pointers.html
内容:
1. 概述
2. 介绍
3. 成员函数指针不仅仅是简单的地址
4. 缓存抉择的结果
5. 成员函数指针的效率
6. 详细说明使用成员函数指针
1. 概述
成员函数指针是c++中众多比较少用的特性之一,甚至很有经验的开发者也不能很好的理解,这可以谅解,因为他们的语法确实比较难用和难以理解。
尽管成员函数指针应用不是很广泛,但有时候他们对于解决特定的问题是很有帮助的而且他们确实是最好的选择,因为他们能够提高效率并且使代码更加合理。他们非常适合缓存那种抉择的结果,用来实现一个不同的排序算法
(这句不会翻译,理解着来吧:They work very well to cache the result of a frequently made decision, and to implement a different sort of polymorphism.)
我将要讨论什么是成员函数指针,如何声明和使用他们,并且给出一些应用他们很好地解决问题的例子
2. 介绍
我不能确切说出成员函数指针的使用频率。当我在usenet和邮件列表里看到一些人提及成员函数指针的时候,我也确实发现和我一起工作的有些人在代码里使用他们,所以我的看法是他们不是很普遍的使用。
成员函数指针重要是因为他们提供了一种有效的方法去缓存是哪个成员函数被调用。在有些情况下这能节省时间,他们提供了另外:一种设计上的选择:避免使用内存分配去缓存那样的抉择(比如是哪个成员函数被调用)。在下面我会深入介绍这些。
成员函数指针允许我们间接地调用对象的一组拥有共同特征成员函数中的一个。
(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. )
我把地址用引号括起来是因为保存在成员函数指针里的不简单是成员函数代码的起始地址,从概念上来讲,他只是一个在类内部定义的函数列表的偏移量,就虚函数表来说,成员函数指针包含的是真正的“虚函数指针表”的偏移。
成员函数指针不能被直接使用他们自身解引用(比如 *pfnCall() 这样的函数调用),他们必须和一些对象一起使用,也就让这些对象提供"this"指针去调用它们的成员函数。
为了说明如何声明和调用一个成员函数指针,我开始先声明并解引用一个普通的非成员函数指针。你可以通过给定一个指针能指向的函数类型来声明一个函数指针,把函数名用“ (*pointerName)”。正常的函数指针在c和c++中有相同的语法:
void Foo(int anInt, double aDouble);
void Bar()
{
void (*funcPtr)(int, double) = &Foo;
(*funcPtr)(1, 2.0);
}
对于正常的函数指针取地址符"&"是可选的,但对于成员函数这是必须的。g++将编译保留他的源码,但会给出一个警告
为了声明一个成员函数指针,你必须像前面那样给出一个要指向的函数类型,但函数名被一个域指针的构造替代---给出他要指向的类的成员函数名,比如(ClassName::*pointerName)。注意一个成员函数指针只能指向他声明的那个类的成员函数,他不能被使用在不同的类上,即使这个类有相同的成员函数特征。
你可以在左侧提供一个解引用或者指向对象的指针,在右侧指定函数指针,使用" .* "或者" ->* " 解引用一个成员函数指针,这有一个简单的例子:
class Foo
{
public:
double One(long inVal);
double Two(long inVal);
};
void main(int argc, char **argv)
{
double (Fool::*funcPtr)(long) = &Foo::One;
Fool aFoo;
double result = (aFoo.*funcPtr)(2);
return 0;
}
除非你暂时使用成员函数指针,否则像上面那样声明一个成员函数指针是很难懂的并且很难正确的运行。像我下面的例子那样使用typedef而不是每次都使用整个函数类型去声明是很有帮助的。
3. 成员函数指针不仅仅是简单的地址
大多数c和c++程序员都知道假设一个指针和int有相同的大小这种做法是很糟糕的风格,然而这种情况却经常出现。不同类型的指针的大小可能不同,这种差别可能不太明显。比如,在x86的16位程序设计中的近指针和远指针可能有不同的大小,远指针由段和偏移构成,而近指针仅仅有偏移。 成员函数指针通常是一个小结构, 这个结构给出了是否虚函数,多继承等信息。
在下面的例子中,使用g++ 2.95.2 在PowerPC G3 Mac OS X iBook上编译,我发现我创建的成员函数指针是8个字节。
这会令使用者感到惊奇。例如,vc6允许程序员做一个优化(默认是开启的),这可能导致相同类型的成员函数指针在不同的编译条件下产生不同的大小。使用错误的设置可能导致臃肿的代码产生bug,因为一个由函数返回的成员函数指针大小很可能不是函数的本来期望的大小,这会导致栈上的数据被虚假的数据覆盖。
在vc的工程设置里有一个标签 "representation",这个标签有一个选择: “best case always”和"most general always",如果你在vc++中使用成员函数指针,请检查这些设置的文档说明,做一个适合的选择,如果可疑的话,就选择"most general always"。
(我找了半天才发现这个选项。。。vc6的)

4. 缓存抉择的结果
一个最好的使用成员函数指针的例子就是缓存在不同的环境下应该使用哪个成员函数这个抉择的结果。以成员函数指针的形式保存可以节省时间,特别是当这种情况发生在一个循环里的时候。
这里有一个很可笑的程序(但希望能简单)显示了使用成员函数指针解决保存抉择结果的问题,他也说明了如何使用typedef:
#include <stdlib.h>
#include <iostream>
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)() <<endl;
return 0;
}
long Test::TimesOne() const
{
return mVal;
}
long Test::TimesTwo() const
{
return 2 * mVal;
}
long Test::TimesThree() const
{
return 3 * mVal;
}
现在我给出不太一样的一个例子,因为他能在一个循环里执行选择很多次,总是到达相同的选择。他是使用成员函数指针进行重构的一个很好的说明。
#include <exception>
class 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 );
}
在大多数情况下在这个循环里都会有相同的选择,一个更好的重构代码是使抉择在循环的外面,并且循环的每个分支都有一个重复的循环(或者被子函数包裹着)
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;
//...
}
}
如果你想避免每个分支里的循环实现,使代码更简单,可以把它们放到一个子函数里。
如果这种重构方式不实用,成员函数指针是最好的解决方案。一个原因可能是因为在代码里循环和抉择属于不同的类,并且你不想暴露出做出抉择的类的实现。这里有使用成员指针对MultiplyIt 代码的重构:
#include <exception>
class 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 < 1000000; ++i )
product = myTest.MultiplyIt( i );
}
5. 成员函数指针的效率
不幸的是,通过解引用成员函数指针调用成员函数比简单的jmp到一个寄存器要复杂的多,这些指针实际上是一些小结构,结构中的一些bit用来查找实际要跳转到的函数地址。我想如果我手上有g++的源代码,我或许能给你演示一下具体实现。我通过跟踪这些成员函数指针的调用发现每个调用都运行一小段汇编代码,这是非常快的代码,如果这段代码保存在cpu的一级缓存里,在循环里执行这些代码也是非常快的。但是他不如简单的比较和条件分支快。
如果你的代码抉择在不断地重复着,使用成员函数指针或许没有什么优势。一个简单的做法是用if语句比较两个数字或者检查bool值,或者用一个每个分支都包含一个小循环的switch语句(构造一个跳表对于编译器是很容易的),可能比解引用成员函数指针更快。如果一个抉择很复杂或者需要经过很长的代码才能到达,比如字符串比较或者搜索一些数据结构,使用成员函数指针可能更好一些。
6. 详细说明使用成员函数指针
如果你看到成员函数指针可以被各种有相同函数调用类型(参数,返回值类型一样),不同实现类型的函数地址赋值(前面有virtual,static,inline等修饰),你可能已经理解了使用结构实现成员函数指针的原因。
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;
}
当你看到我创建一个指向inline函数的指针时,可能感到惊讶,但如果你想这样做,确实是可以的,编译器将会放置一个inline函数的函数实现版本,并把它的地址给你,所以函数指针根本不真正指向inline函数。
尽管看上去一个静态成员函数也可以做相同的转换,但实际上根本不会,因为他没有被传递给指针,他像其他参数一样被传递给了你的成员函数,但他没有明确指明函数类型你不能使用成员函数指针保存一个静态函数的地址(使用普通的,非成员函数指针去保存)。
void Fails()
{
Different::FuncPtr ptr = &Different::StaticMember;
}
指向虚函数的成员函数指针工作起来就像直接调用虚函数一样,成员函数的类型被动态地从对象获得,而不是成员函数指针所代表的静态类型:
#include <iostream>
class 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" << std::endl;
}
void Derived::WhoAmI() const
{
std::cout << "I am the Derived" << std::endl;
}
int main( int argc, char **argv )
{
Base::WhoPtr func = &Base::WhoAmI;
Base theBase;
(theBase.*func)();
Derived theDerived;
(theDerived.*func)();
return 0;
}
运行上面的程序输出结果如下:
I am the Base
I am the Derived
多态被认为是通过继承包含虚成员函数的类实现的。一个继承类对象可以被赋值给基类的指针或引用;当通过指针或引用调用虚成员函数的时候,函数指针会查找实际被分配的对象的虚函数表,而不是像声明的基类指针或引用那样的静态类型。
然而多态的概念可以有更普遍的意义,并且我在邮件列表里看到建议他应该包含模板的使用:允许相同的源代码被应用于不同的没有关系的对象类型。当一个vector被声明的时候,他被认为是一个类型被参数化的多态容器。
成员函数指针可以被用来实现各种不同的多态。在常规类型中,我们通过分配不同类型的对象来决定继承树中哪个相关的成员函数被调用,这是通过一个隐藏在对象里的虚函数表实现的。
在总是创建同一类型对象的情况下,可以通过赋值给成员函数指针哪个成员函数地址决定哪个成员函数被调用,一个有趣的好处是你可以不必分配一个以普通继承为基础的多态类型的对象,却可以在运行期改变对象的行为
(上面的意思就是不需要动态分配新的继承树中的对象就可以调用这个对象的成员函数)