跟我学C++中级篇——模板友元的应用

一、友元

友元在以前分析过,而且一般编程是不推荐使用友元的,原因是友元破坏了类的封装性。但凡事总有例外,在某些情况下,用友元还是比较方便的,那么该用还得用,不能因噎废食。普通的友元,各种书籍资料和网上多得很。一般来说,可以把友元分为两大类:
1、普通的友元
这种友元的使用,不管是类还是函数,开发者都相对比较熟悉,此处就不再赘述,如果有不明白的可以 翻一下书籍。一般来说,即使在模板里,使用特化的或者实例化的模板类或者函数,也看做是普通友元。模板只要特化或者说实例化就和普通类或者函数没有什么区别了。
2、模板的友元
这里重点分析一下模板中使用模板函数或者模板类做为友元时的处理情况,这里面涉及到一些具体的编译器的不同处理方式,需要引起重视。

二、模板函数做为友元

老规矩行先看代码:

#include 

//第一种情况
template <typename T> class FriendF {
public:
  T t_;

public:
  FriendF() { t_ = T(1.9); }
  // VS OK
  // template  friend void Display(const FriendF &f);
  template <typename U> friend void Display(const FriendF<U> &f);
};

template <typename U> void Display(const FriendF<U> &f) {
  std::cout << "cur value is:" << f.t_ << std::endl;
}

int friendtest() {
  FriendF<int> f;
  Display<int>(f);
  return 0;
}
//
//第二种情况
template <typename T> class FriendF1;
template <typename T> void Display(const FriendF1<T> &a);

template <typename T> class FriendF1 {
  T t_;

public:
  FriendF1() { t_ = T(1.6); }
  friend void Display<T>(const FriendF1<T> &f);
};

template <typename T> void Display(const FriendF1<T> &f) {
  std::cout << "cur value is:" << f.t_ << std::endl;
}

int friendtest1() {
  FriendF1<int> f1;
  Display<int>(f1);
  return 0;
}
///
//第三种情况

template <typename T> class FriendF2 {
  T t_;

public:
  FriendF2() { t_ = T(2.0f); }
  friend void Display(const FriendF2<T> &f2) {
    std::cout << f2.t_ << std::endl;
  }
};

int friendtest2() {
  FriendF2<int> f2;
  Display(f2);
  return 0;
}

///
//第四种情况
template <typename T> class NData;
template <typename T> void swap1(NData<T> &n1, NData<T> &n2);
template <typename T> void Display1(NData<T> &n1, NData<T> &n2);
template <typename T> void swap(NData<T> &n1, NData<T> &n2);

template <typename T> class NData {
public:
  NData() {}
  NData(T t) : t_(t) {}

public:
  NData<T> *left_;
  NData<T> *right_;
  T t_;

public:
  friend void swap1<>(NData<T> &, NData<T> &);
  friend void swap<>(NData<T> &, NData<T> &);
  friend void Display1<>(NData<T> &, NData<T> &);
};
template <typename T> void Display1(NData<T> &n1, NData<T> &n2) {
  std::cout << n1.t_ << std::endl;
}
template <typename T> void swap1(NData<T> &n1, NData<T> &n2) {
  std::swap(n1.left_, n2.left_);
  std::swap(n1.right_, n2.right_);
  std::swap(n1.t_, n2.t_);
  std::cout << "vlaue is:" << n1.t_ << std::endl;
}
template <typename T> void swap(NData<T> &n1, NData<T> &n2) {
  //   // std::swap(n1.left_, n2.left_);
  //   // std::swap(n1.right_, n2.right_);
  //   std::swap(n1.t_, n2.t_);
  std::cout << "n1,b" << n1.t_ << std::endl;
}

int friendtest3() {
  NData<int> nd1;
  NData<int> nd2;

  swap(nd1, nd2);
  swap1(nd1, nd2);
  Display1(nd1, nd2);
  return 0;
}
int main()
{
	friendtest();
	friendtest1();
	friendtest2();
	friendtest3();
}

在模板函数做为友元的情况下,分成了三种情况两大类:
1、模板类内声明,模板外实现
第一种情况就是这种,在类声明了模板函数为友元,但是在类外实现函数体。
2、模板类内声明,类内实现
第三种就是在类内直接实现模板函数友元的函数体。
3、模板类外声明
第二种情况就是把友元模板函数在类外进行声明,有点类似于头文件的作用,然后在类中声明友元,类外进行函数体的实现。第四种情况中,其实和第二种情况基本没有区别。只是用<>来表明这是一种模板函数的具现化。

*这里需要说明一下"friend void swap1<>(NData &, NData &)"这种<>尖括号声明,用侯捷老师的书中的原话是“使用空的template argument list,这种形式告诉编译器,只从template具现体中挑选合适的呼叫对象,所有template parameters 都自 call parameters 推导而得”。所以才在下面说第四种情况视情况而定的原因。
其实它的意思就是说,这个是要从具体的调用参数推导出来。也就是说,常规的函数是允许参数自动类型转换,但模板参数推断时不允许,这就等于在实际推导时要根据已经生成的挑选(推导)最匹配的。

两大类是从另外一种角度看:
1、可以直接在类内声明限定约束(bound)模板友元的参数类型
也就是说,友元模板参数要和应用 其类的模板参数保持一致。第一种情况的注释版第二种及第三种情况都是约束的情况。这种也容易为开发者理解和应用。
2、模板未对友元模板进行约束(unbound)
这种情况就是第一种情况,第一种情况注释的部分在vs上可以 编译应用 ,但在Gcc和Clang上编译会出现“Declaration of ‘T’ shadows template parameter”,这个在前面也分析过(“模板的模板参数的匹配”)。
第四种情况视情况而定吧。

三、模板类做为友元

而模板类做为友元可以 分为两种情况:

//第一种情况
template <typename T> class FriendB;

template <typename T> class FriendA {
public:
  T t_;

public:
  FriendA() { t_ = T(3.6); }
  friend class FriendB<T>;
};
//第二种情况
template <typename T> class FriendAA {
public:
  T t_;

public:
  FriendAA() { t_ = T(100); }
  template <class U> friend class FriendBB;
};
template <typename U> class FriendBB {
public:
  static void Display(const FriendAA<U> &faa) {
    std::cout << "cur value is:" << faa.t_ << std::endl;
  }
};
template <typename T> class FriendB {
public:
  static void Display(const FriendA<T> &fa) {
    std::cout << "cur value is::" << fa.t_ << std::endl;
  }
};

int friendclass() {
  FriendA<int> aa1;
  FriendAA<double> aa2;
  FriendB<int>::Display(aa1);
  FriendBB<double>::Display(aa2);
  return 0;
}
int main()
{
friendclass();
}

它们两种情况都是在类声明为友元,但是第一种情况中在前声明友元前进行了友元模板类的前向声明;第二种情况则直接在类内声明。前者更类似于一种实例化的类,在前面的学习中知道,在模板类中,声明的模板形参可以在内部认为已经实例化,直接使用。而第二种则和前面函数模板类似,属于不受约束的方式,是一种声明形式。
也就是说,第一种是码元模板参数受约束的形式,而第二种则为不受约束的形式。

四、问题

在这次的例程编写中,遇到了一个基础问题,觉得对初学者甚至中等水平开发者还是有一定意义的,这里展示出来。

......

template <typename T> class NData {
public:
  NData() {}
  NData(T t) : t_(t) {}

public:
  NData<T> *left_;
  NData<T> *right_;
  T t_;

public:
  friend void swap1<>(NData<T> &, NData<T> &);
  friend void swap<>(NData<T> &, NData<T> &);
......
};

template <typename T> void swap1(NData<T> &n1, NData<T> &n2) {
  std::swap(n1.left_, n2.left_);
  std::swap(n1.right_, n2.right_);
  std::swap(n1.t_, n2.t_);
  std::cout << "vlaue is:" << n1.t_ << std::endl;
}
template <typename T> void swap(NData<T> &n1, NData<T> &n2) {
  //   // std::swap(n1.left_, n2.left_);
  //   // std::swap(n1.right_, n2.right_);
  //   std::swap(n1.t_, n2.t_);
  std::cout << "n1,b" << n1.t_ << std::endl;
}

也就是函数模板做为友元的第四种情况中,使用swap做函数名时,总是报"assertion failed: template argument must be a complete class or an unbounded array"。有点摸不着头脑,类NData是一个完全定义好的啊。后来突然想到会不会是名空间污染的问题(std::swap())。于是又写了一个swap1,结果没问题。验证与想的完全一致,回头查看一下代码,在工程中被包含的cpp文件中声明了using namespae std,所以在实际工程中,尽量增加自定义的名字空间,并且不要使用类似“using namespae std”这种偷懒的方式来编程。

五、总结

C++语言更需要多实践,多干活自然就会有各种情况遇到,和所学的知识不断的验证、巩固。查遗补缺,技术的增长就会越来越快,光死记理论,时间长了要么忘记了,要么记混了,写起代码来还是有无法驾驭的情况。

你可能感兴趣的:(C++,C++11,c++,开发语言)