2.9 扩展的friend语法

类别:部分人

friend关键字在C++中是一个比较特别的存在。因为我们常常会发现,一些面向对象程序语言,比如Java,就没有定义friend关键字。friend关键字用于声明类的友元,友元可以无视类中成员的属性。无论成员是public、protected或是private的,友元类或友元函数都可以访问,这就完全破坏了面向对象编程中封装性的概念。因此,使用friend关键字充满了争议性。在通常情况下,面向对象程序开发的专家会建议程序员使用Get/Set接口来访问类的成员,但有的时候,friend关键字确实会让程序员少写很多代码。因此即使存在争论,friend还是在很多程序中被使用到。而C++11对friend关键字进行了一些改进,以保证其更加好用。我们可以看看下面的例子,如代码清单2-19所示。

代码清单2-19


class Poly;

typedef Poly P;

class LiLei{

friend class Poly;//C++98通过,C++11通过

};

class Jim{

friend Poly;//C++98失败,C++11通过

};

class HanMeiMei{

friend P;//C++98失败,C++11通过

};

//编译选项:g++ -std=c++11 2-9-1.cpp


在代码清单2-19中,我们声明了3个类型:LiLei、Jim和HanMeiMei,它们都有一个友元类型Poly。从编译通过与否的状况中我们可以看出,在C++11中,声明一个类为另外一个类的友元时,不再需要使用class关键字。本例中的Jim和HanMeiMei就是这样一种情况,在HanMeiMei的声明中,我们甚至还使用了Poly的别名P,这同样是可行的。

虽然在C++11中这是一个小的改进,却会带来一点应用的变化——程序员可以为类模板声明友元了。这在C++98中是无法做到的。比如下面这个例子,如代码清单2-20所示。

代码清单2-20


class P;

template<typename T>class People{

friend T;

};

People<P>PP;//类型P在这里是People类型的友元

People<int> Pi;//对于int类型模板参数,友元声明被忽略

//编译选项:g++ -std=c++11 2-9-2.cpp


从代码清单2-20中我们看到,对于People这个模板类,在使用类P为模板参数时,P是People<P>的一个friend类。而在使用内置类型int作为模板参数的时候,People<int>会被实例化为一个普通的没有友元定义的类型。这样一来,我们就可以在模板实例化时才确定一个模板类是否有友元,以及谁是这个模板类的友元。这是一个非常有趣的小特性,在编写一些测试用例的时候,使用该特性是很有好处的。我们看看下面的例子,该例子源自一个实际的测试用例,如代码清单2-21所示。

代码清单2-21


//为了方便测试,进行了危险的定义

ifdef UNIT_TEST

define private public

endif

class Defender{

public:

void Defence(int x,int y){}

void Tackle(int x,int y){}

private:

int pos_x=15;

int pos_y=0;

int speed=2;

int stamina=120;

};

class Attacker{

public:

void Move(int x,int y){}

void SpeedUp(float ratio){}

private:

int pos_x=0;

int pos_y=-30;

int speed=3;

int stamina=100;

};

ifdef UNIT_TEST

class Validator{

public:

void Validate(int x,int y,Defender&d){}

void Validate(int x,int y,Attacker&a){}

};

int main(){

Defender d;

Attacker a;

a.Move(15,30);

d.Defence(15,30);

a.SpeedUp(1.5f);

d.Defence(15,30);

Validator v;

v.Validate(7,0,d);

v.Validate(1,-10,a);

return 0;

}

endif

//编译选项:g++2-9-3.cpp-std=c++11-DUNIT_TEST


在代码清单2-21所示的这个例子中,测试人员的目的是在一系列函数调用后,检查Attacker类变量a和Defender类变量d中成员变量的值是否符合预期。这里,按照封装的思想,所有成员变量被声明为private的。但Attacker和Defender的开发者为了方便,并没有为每个成员写Get函数,也没有为Attacker和Defender增加友元定义。而测试人员为了能够快速写出测试程序,采用了比较危险的做法,即使用宏将private关键字统一替换为public关键字。这样一来,类中的private成员就都成了public的。这样的做法存在4个缺点:一是如果侥幸程序中没有变量包含private字符串,该方法可以正常工作,但反之,则有可能导致严重的编译时错误;二是这种做法会降低代码可读性,因为改变了一个常见关键字的意义,没有注意到这个宏的程序员可能会非常迷惑程序的行为;三是如果在类的成员定义时不指定关键字(如public、private、protect等),而使用默认的private成员访问限制,那么该方法也不能达到目的;四则很简单,这样的做法看起来也并不漂亮。

不过由于有了扩展的friend声明,在C++11中,我们可以将Defender和Attacker类改良一下。我们看看下面的例子,如代码清单2-22所示。

代码清单2-22


template<typename T>class DefenderT{

public:

friend T;

void Defence(int x,int y){}

void Tackle(int x,int y){}

private:

int pos_x=15;

int pos_y=0;

int speed=2;

int stamina=120;

};

template<typename T>class AttackerT{

public:

friend T;

void Move(int x,int y){}

void SpeedUp(float ratio){}

private:

int pos_x=0;

int pos_y=-30;

int speed=3;

int stamina=100;

};

using Defender=DefenderT<int> ;//普通的类定义,使用int做参数

using Attacker=AttackerT<int> ;

ifdef UNIT_TEST

class Validator{

public:

void Validate(int x,int y,DefenderTest&d){}

void Validate(int x,int y,AttackerTest&a){}

};

using DefenderTest=DefenderT<Validator>;//测试专用的定义,Validator类成为友元

using AttackerTest=AttackerT<Validator>;

int main(){

DefenderTest d;

AttackerTest a;

a.Move(15,30);

d.Defence(15,30);

a.SpeedUp(1.5f);

d.Defence(15,30);

Validator v;

v.Validate(7,0,d);

v.Validate(1,-10,a);

return 0;

}

endif

//编译选项:g++2-9-4.cpp-std=c++11-DUNIT_TEST


在代码清单2-22中,我们把原有的Defender和Attacker类定义为模板类DefenderT和AttackerT。而在需要进行测试的时候,我们使用Validator为模板参数,实例化出DefenderTest及AttackerTest版本的类,这个版本的特点是,Validator是它们的友元,可以任意访问任何成员函数。而另外一个版本则是使用int类型进行实例化的Defender和Attacker,按照C++11的定义,它们不会有友元。因此这个版本保持了良好的封装性,可以用于提供接口用于常规使用。

值得注意的是,在代码清单2-22中,我们使用了using来定义类型的别名,这跟使用typedef的定义类型的别名是完全一样的。使用using定义类型别名是C++11中的一个新特性,我们可以在3.10节中看到相关的描述。