(轉貼) ATL布幔之下的秘密(3) (C/C++) (template)

轉貼自VC知識文庫 ATL布幔之下的秘密(3)

作者:Zeeshan Amjad
译者:李马 (home.nuc.edu.cn/~titilima )
原文出处: http://www.codeproject.com/atl/atl_underthehood_3.asp

介绍

   如果你是个模板的高手,你就可以将ATL的学习作为一种享受。在这一节中,我将要尝试解释一些ATL使用的模板技术。我不能保证你读完本节后能成为一个模板高手,只能是尽我所能让你在读完本文后能够更轻松地理解ATL的源码。

程序35.

#include <iostream>
using namespace std;

template <typename T>
T Maximum(const T& a, const T& b) {
  return a > b ? a : b;
}
int main() {
  cout << Maximum(5, 10) << endl;
  cout << Maximum(''A'', ''B'') << endl;

  return 0;
}
程序的输出为:
10
B
在这里,由于模板函数的关系,我们就没有必要分别重载int和char数据类型的函数版本了。其中很重要的一点是,函数的两个参数类型必须一致。但是如果我们传入了不同的数据类型,我们就需要告知编译器应该把这个参数考虑为哪种数据类型。

程序36.
#include <iostream>
using namespace std;

template <typename T>
T Maximum(const T& a, const T& b) {
  return a > b ? a : b;
}

int main() {
  cout << Maximum<int>(5, ''B'') << endl;
  cout << Maximum<char>(5, ''B'') << endl;

  return 0;
}
程序的输出为:
66
B
我们也可以编写类模板,下面就是一个简单版本的堆栈类模板。

程序37.
#include <iostream>
using namespace std;

template <typename T>
class Stack {
private:
  T* m_pData;
  int m_iTop;

public:
  Stack(int p_iSize = 0) : m_iTop(0) {
    m_pData = new T[p_iSize];
  }

  void Push(T p_iData) {
    m_pData[m_iTop++] = p_iData;
  }

  T Pop() {
    return m_pData[--m_iTop];
  }

  T Top() {
    return m_pData[m_iTop];
  }

  ~Stack() {
    if (m_pData) {
      delete [] m_pData;
    }
  }

private:
  Stack(const Stack<T>&);
  Stack<T>& operator = (const Stack<T>&);
};

int main() {
  Stack<int> a(10);

  a.Push(10);
  a.Push(20);
  a.Push(30);

  cout << a.Pop() << endl;
  cout << a.Pop() << endl;
  cout << a.Pop() << endl;
  
  return 0;
}
这个程序中没有任何错误检验,不过这个程序的目的只是示范模板的用法,而不是真的要写一个专业的堆栈类。
   程序的输出为:
30
20
10
我们也可以将数据类型作为一个模板参数来传递,并且为它设置一个默认值。让我们来稍微修改一下程序37(译注:原文为“程序36”,应为37),并将堆栈的尺寸作为一个模板参数来传递,而不是作为构造函数的参数。

程序38.
#include <iostream>
using namespace std;

template <typename T, int iSize = 10>
class Stack {
private:
  T m_pData[iSize];
  int m_iTop;

public:
  Stack() : m_iTop(0) {
  }

  void Push(T p_iData) {
    m_pData[m_iTop++] = p_iData;
  }

  T Pop() {
    return m_pData[--m_iTop];
  }

  T Top() {
    return m_pData[m_iTop];
  }

private:
  Stack(const Stack<T>&);
  Stack<T>& operator = (const Stack<T>&);
};

int main() {
  Stack<int, 10> a;

  a.Push(10);
  a.Push(20);
  a.Push(30);

  cout << a.Pop() << endl;
  cout << a.Pop() << endl;
  cout << a.Pop() << endl;
  
  return 0;
}
程序的输出和前一个相同。这个程序最重要的一点为:
template <typename T, int iSize = 10>
现在就有一个问题:哪一个更好呢?通常,传递模板参数的办法是优于给构造函数传递参数的。为什么呢?因为在你将堆栈尺寸作为模板参数传递的时候,这个给定数据类型的数组就会被自动创建;而给构造函数传递参数则意味着构造函数会在运行时使用new或malloc一系列功能来分配内存。如果我们已经确定在创建好堆栈之后就不再更改它的尺寸(就像上面程序中private段中拷贝构造函数和赋值运算符中的那样)了,那么无疑使用模板参数是更加适合的。
   (译注:作者Amjad在上面两个程序中并未实现拷贝构造函数和赋值运算符,这大概是由于这两者对于本文的内容无关紧要之故吧。在此我要指出的是正如作者所说,“不是真的要写一个专业的堆栈类”、“没有任何错误检验”,并且这其中类的组织结构使得精确实现拷贝构造函数和赋值运算符有一定的难度,尤其是程序37——我们无法从一个已经定义好的堆栈获得它的最大容量。)
   你也可以将用户定义的类作为一个类型参数来传递,但是请确认这个类拥有在那个模板函数或类模板中重载的所有运算符。
   例如,请看程序35那个求最大值的函数。这个程序使用了一个operator >,所以如果我们传递自己的类的话,那么这个类必须重载了>运算符。下面这个例子示范了这一点。

程序39.
#include <iostream>
using namespace std;

template <typename T>
T Maximum(const T& a, const T& b) {
  return a > b ? a : b;
}

class Point {
private:
  int m_x, m_y;

public:
  Point(int p_x = 0, int p_y = 0) : m_x(p_x), m_y(p_y) {
  }

  bool friend operator > (const Point& lhs, const Point& rhs) {
    return lhs.m_x > rhs.m_x && lhs.m_y > rhs.m_y;
  }

  friend ostream& operator << (ostream& os, const Point& p) {
    return os << "(" << p.m_x << ", " << p.m_y  << ")";
  }
};

int main() {
  Point a(5, 10), b(15, 20);
  cout << Maximum(a, b) << endl;

  return 0;
}
程序的输出为:
(15, 20)
同样,我们也能够将一个类模板作为一个模板参数传递。现在让我们来编写这样一个Point类,并将其作为一个模板参数传递给Stack类模板。

程序40.
#include <iostream>
using namespace std;

template <typename T>
class Point {
private:
  T m_x, m_y;

public:
  Point(T p_x = 0, T p_y = 0) : m_x(p_x), m_y(p_y) {
  }

  bool friend operator > (const Point<T>& lhs, const Point<T>& rhs) {
    return lhs.m_x > rhs.m_x && lhs.m_y > rhs.m_y;
  }

  friend ostream& operator << (ostream& os, const Point<T>& p) {
    return os << "(" << p.m_x << ", " << p.m_y  << ")";
  }
};

template <typename T, int iSize = 10>
class Stack {
private:
  T m_pData[iSize];
  int m_iTop;

public:
  Stack() : m_iTop(0) {
  }

  void Push(T p_iData) {
    m_pData[m_iTop++] = p_iData;
  }

  T Pop() {
    return m_pData[--m_iTop];
  }

  T Top() {
    return m_pData[m_iTop];
  }

private:
  Stack(const Stack<T>&);
  Stack<T>& operator = (const Stack<T>&);
};

int main() {
  Stack<Point<int> > st;

  st.Push(Point<int>(5, 10));
  st.Push(Point<int>(15, 20));

  cout << st.Pop() << endl;
  cout << st.Pop() << endl;

  return 0;
}
程序的输出为:
(15, 20)
(5, 10)
这个程序中最重要的部分为:
Stack<Point<int> > st;
在这里,你必须在两个大于号之间放置一个空格,否则编译器就会将它看作>>(右移运算符)并产生错误。
   对于这个程序我们还可以这么做,就是为模板参数传递默认的类型值,也就是将
template <typename T, int iSize = 10>
换为
template <typename T = int, int iSize = 10>
现在我们就没有必要一定在创建Stack类对象的时候传递数据类型了,但是你仍然需要书写这一对尖括弧以告知编译器使用默认的数据类型。你可以这么创建对象:
Stack<> st;
当你在类的外部定义(译注:原文此处是“declare”,我以为应该是“define”更准确一些。)类模板的成员函数的时候,你仍然需要写出带有模板参数的类模板全称。

程序41.
#include <iostream>
using namespace std;

template <typename T>
class Point {
private:
  T m_x, m_y;

public:
  Point(T p_x = 0, T p_y = 0);
  void Setxy(T p_x, T p_y);
  T getX() const;
  T getY() const;

  friend ostream& operator << (ostream& os, const Point<T>& p) {
    return os << "(" << p.m_x << ", " << p.m_y  << ")";
  }
};

template <typename T>
Point<T>::Point(T p_x, T p_y) : m_x(p_x), m_y(p_y) {
}

template <typename T>
void Point<T>::Setxy(T p_x, T p_y) {
  m_x = p_x;
  m_y = p_y;
}

template <typename T>
T Point<T>::getX() const {
  return m_x;
}

template <typename T>
T Point<T>::getY() const {
  return m_y;
}

int main() {
  Point<int> p;
  p.Setxy(20, 30);
  cout << p << endl;

  return 0;
}
程序的输出为:
(20, 30)
让我们来稍微修改一下程序35,传递字符串值(而不是int或float)作为参数,并看看结果吧。

程序42.
#include <iostream>
using namespace std;

template <typename T>
T Maximum(T a, T b) {
  return a > b ? a : b;
}

int main() {
  cout << Maximum("Pakistan", "Karachi") << endl;
  
  return 0;
}
程序的输出为Karachi。(译注:在我的Visual Studio.net 2003下的输出却为Pakistan,这不同的原因是编译器组织字符串地址的方式不同决定的,但是Maximum函数的结果是应该返回内存高位的那个地址的,这和作者说的道理是一致的。)为什么呢?因为这里char*作为模板参数传递, Karachi在内存中存储的位置更高,而>运算符仅仅比较这两个地址值而不是字符串本身。
   那么,如果我们希望基于字符串的长度来比较而不是地址的话,应该怎么做呢?
   解决的办法是对char*数据类型进行模板的特化。下面是一个模板特化的例子。

程序43.
#include <iostream>
using namespace std;

template <typename T>
T Maximum(T a, T b) {
  return a > b ? a : b;
}

template <>
char* Maximum(char* a, char* b) {
  return strlen(a) > strlen(b) ? a : b;
}

int main() {
  cout << Maximum("Pakistan", "Karachi") << endl;
  
  return 0;
}
至于类模板,也可以用相同的办法进行特化。

程序44.
#include <iostream>
using namespace std;

template <typename T>
class TestClass {
public:
  void F(T pT) {
    cout << "T version" << ''\t'';
    cout << pT << endl;
  }
};

template <>
class TestClass<int> {
public:
  void F(int pT) {
    cout << "int version" << ''\t'';
    cout << pT << endl;
  }
};

int main() {
  TestClass<char> obj1;
  TestClass<int> obj2;

  obj1.F(''A'');
  obj2.F(10);

  return 0;
}
程序的输出为:
T version  A
int version  10
ATL中就有若干类是类似这样的特化版本,例如在ATLBASE.H中定义的CComQIPtr。
   模板也可以在不同的设计模式中使用,例如策略设计模式可以使用模板实现。

程序45.
#include <iostream>
using namespace std;

class Round1 {
public:
  void Play() {
    cout << "Round1::Play" << endl;
  }
};

class Round2 {
public:
  void Play() {
    cout << "Round2::Play" << endl;
  }
};

template <typename T>
class Strategy {
private:
  T objT;
public:
  void Play() {
    objT.Play();
  }
};

int main() {
  Strategy<Round1> obj1;
  Strategy<Round2> obj2;

  obj1.Play();
  obj2.Play();

  return 0;
}
在这里,Round1和Round2为一个游戏中不同的关卡类,并且Strategy类依靠传递的模板参数来决定该做些什么。
   程序的输出为:
Round1::Play
Round2::Play
ATL就是使用Strategy设计模式来实现线程的。
   代理设计模式也可以使用模板实现,智能指针就是一个例子。下面就是一个没有使用模板的简单版本智能指针。

程序46.
#include <iostream>
using namespace std;

class Inner {
public:
  void Fun() {
    cout << "Inner::Fun" << endl;
  }
};

class Outer {
private:
  Inner* m_pInner;

public:
  Outer(Inner* p_pInner) : m_pInner(p_pInner) {
  }

  Inner* operator -> () {
    return m_pInner;
  }
};

int main() {
  Inner objInner;
  Outer objOuter(&objInner);

  objOuter->Fun();

  return 0;
}
  
程序的输出为:
Inner::Fun()
简单地说来,我们仅仅重载了->运算符,但是在实际的智能指针中,所有必须的运算符(例如=、==、!、&、*)都需要被重载。以上的智能指针有一个大问题:它只能包含指向Inner对象的指针。我们可以编写Outer类模板来取消这一限制,现在让我们来略微修改一下程序。

程序47.
#include <iostream>
using namespace std;

class Inner {
public:
  void Fun() {
    cout << "Inner::Fun" << endl;
  }
};

template <typename T>
class Outer {
private:
  T* m_pInner;

public:
  Outer(T* p_pInner) : m_pInner(p_pInner) {
  }

  T* operator -> () {
    return m_pInner;
  }
};

int main() {
  Inner objInner;
  Outer<Inner> objOuter(&objInner);

  objOuter->Fun();

  return 0;
}
程序的输出和前一个一样,但是现在Outer类就可以包含任何类型了,只需要把类型作为模板参数传递进来即可。
   ATL中有两个智能指针,CComPtr和CComQIPtr。
   你可以用模板做一些有趣的事情,例如你的类可以在不同的情况下成为不同基类的子类。

程序48.
#include <iostream>
using namespace std;

class Base1 {
public:
  Base1() {
    cout << "Base1::Base1" << endl;
  }
};

class Base2 {
public:
  Base2() {
    cout << "Base2::Base2" << endl;
  }
};

template <typename T>
class Drive : public T {
public:
  Drive() {
    cout << "Drive::Drive" << endl;
  }
};

int main() {

  Drive<Base1> obj1;
  Drive<Base2> obj2;
  return 0;
}
程序的输出为:
Base1::Base1
Drive::Drive
Base2::Base2
Drive::Drive
在这里,Drive类是继承自Base1还是Base2是由在对象创建的时候传递给模板的参数决定的。
   ATL也使用了这一技术。当你使用ATL创建COM组件的时候,CComObject就会继承自你的类。在这里ATL利用了模板,因为它不会预先知道你用来作COM组件而创建的类的名称。CComObject类定义于ATLCOM.H文件之中。
   在模板的帮助下,我们也可以模拟虚函数。现在让我们重新回忆一下虚函数,下面是一个简单的例子。

程序49.
#include <iostream>
using namespace std;

class Base {
public:
  virtual void fun() {
    cout << "Base::fun" << endl;
  }

  void doSomething() {
    fun();
  }
};

class Drive : public Base {
public:
  void fun() {
    cout << "Drive::fun" << endl;
  }
};

int main() {
  Drive obj;
  obj.doSomething();

  return 0;
}
程序的输出为:
Drive::fun
在模板的帮助下,我们可以实现与之相同的行为。

程序50.
#include <iostream>
using namespace std;

template <typename T>
class Base {
public:
  void fun() {
    cout << "Base::fun" << endl;
  }

  void doSomething() {
    T* pT = static_cast<T*>(this);
    pT->fun();
  }
};

class Drive : public Base<Drive> {
public:
  void fun() {
    cout << "Drive::fun" << endl;
  }
};

int main() {
  Drive obj;
  obj.doSomething();

  return 0;
}
  
程序的输出和前一个是一样的,所以我们可以用模板来模拟虚函数的行为。
   程序中一个有趣的地方为
class Drive : public Base<Drive> {
这表明我们可以将Drive类作为一个模板参数来传递。程序中另外一个有趣的地方是基类中的doSomething函数。
T* pT = static_cast<T*>(this);
pT->fun();
在这里基类的指针被转换为派生类的指针,因为派生类是作为Base类的模板参数传递的。这个函数可以通过指针来执行,由于指针指向了派生类的对象,所以派生类的对象就被调用了。
   但是这就有一个问题了:我们为什么要这样做?答案是:这样可以节省虚函数带有的额外开销,也就是虚函数表指针、虚函数表以及节省了调用虚函数所花费的额外时间。这就是ATL中使组件尽可能小、尽可能快的主要思想。
   现在,你的脑海中可能会浮现另外一个问题。如果依靠这一开销更少的技术可以模拟虚函数的话,那我们为什么还要调用虚函数呢?我们不应该用这一技术替换所有的虚函数吗?对于这一问题,我可以简短地回答你:不,我们不能用这一技术替换虚函数。
   其实这一技术还存在一些问题。第一,你不能从Drive类进行更深层的继承,如果你试着这么做,那么它将不再会是虚函数的行为了。而对于虚函数来说,这一切就不会发生。一旦你将函数声明为虚函数,那么在派生类中的所有函数都会成为虚函数,无论继承链有多深。现在我们看看当从Drive中再继承一个类的时候会发生什么。

程序51.
#include <iostream>
using namespace std;

template <typename T>
class Base {
public:
  void fun() {
    cout << "Base::fun" << endl;
  }

  void doSomething() {
    T* pT = static_cast<T*>(this);
    pT->fun();
  }
};

class Drive : public Base<Drive> {
public:
  void fun() {
    cout << "Drive::fun" << endl;
  }
};

class MostDrive : public Drive {
public:
  void fun() {
    cout << "MostDrive::fun" << endl;
  }
};

int main() {
  MostDrive obj;
  obj.doSomething();

  return 0;
}
程序的输出和前一个一样。但是对于虚函数的情况来说,输出就应该是:
MostDrive::fun
这一技术还有另外一个问题,就是当我们使用Base类的指针来存储派生类的地址的时候。

程序52.
#include <iostream>
using namespace std;

template <typename T>
class Base {
public:
  void fun() {
    cout << "Base::fun" << endl;
  }

  void doSomething() {
    T* pT = static_cast<T*>(this);
    pT->fun();
  }
};

class Drive : public Base<Drive> {
public:
  void fun() {
    cout << "Drive::fun" << endl;
  }
};

int main() {
  Base* pBase = NULL;
  pBase = new Drive;

  return 0;
}
这个程序会给出一个错误,因为我们没有向基类传递模板参数。现在我们稍微修改一下,并传递模板参数。

程序53.
#include <iostream>
using namespace std;

template <typename T>
class Base {
public:
  void fun() {
    cout << "Base::fun" << endl;
  }

  void doSomething() {
    T* pT = static_cast<T*>(this);
    pT->fun();
  }
};

class Drive : public Base<Drive> {
public:
  void fun() {
    cout << "Drive::fun" << endl;
  }
};

int main() {
  Base<Drive>* pBase = NULL;
  pBase = new Drive;
  pBase->doSomething();

  return 0;
}
现在程序正常工作,并给出了我们所期望的输出,也就是:
Drive::fun
但是在Base类有多个继承的时候,就会出现问题。为了更好地弄懂这一点,请看下面的程序。

程序54.
#include <iostream>
using namespace std;

template <typename T>
class Base {
public:
  void fun() {
    cout << "Base::fun" << endl;
  }

  void doSomething() {
    T* pT = static_cast<T*>(this);
    pT->fun();
  }
};

class Drive1 : public Base<Drive1> {
public:
  void fun() {
    cout << "Drive1::fun" << endl;
  }
};

class Drive2 : public Base<Drive2> {
public:
  void fun() {
    cout << "Drive2::fun" << endl;
  }
};

int main() {
  Base<Drive1>* pBase = NULL;
  pBase = new Drive1;
  pBase->doSomething();
  delete pBase;

  pBase = new Drive2;
  pBase->doSomething();

  return 0;
}
程序会在下面的代码处给出错误:
pBase = new Drive2;
因为pBase是一个指向Base<Drive1>的指针,而不是Base<Drive2>。简单地说来,就是你不能使Base类的指针指向不同的Drive类。换句话说,你不能使用Base类指针的数组存储不同的派生类,而在虚函数之中则是可行的。
   希望在下一篇文章中能够探究一些ATL的其它秘密。

posted on 2007-08-28 21:00  真 OO无双  阅读(998)  评论(1编辑  收藏  举报

导航