C++设计模式[二十二]模板方法模式

这是最后一种模式,前面学习文章都是随机按照顺序排的,学到最后一个估计对c++也了解认识了许多了吧,模板顾名思义就是一个模子,通过模子来刻出许多类。

果冻想 | 一个原创文章分享网站

AbstractClass(抽象类):定义抽象的原语操作,具体的子类将重定义它们以实现一个算法的各步骤。主要是实现一个模板方法,定义一个算法的骨架。该模板方法不仅调用原语操作,也调用定义在AbstractClass或其他对象中的操作。
ConcreteClass(具体类):实现原语操作以完成算法中与特定子类相关的步骤。
由于在具体的子类ConcreteClass中重定义了实现一个算法的各步骤,而对于不变的算法流程则在AbstractClass的TemplateMethod中完成。

这个模式比较简单,看一个例子就明白了:

#include"stdafx.h"
#include<string>
#include <iostream>
using namespace std;
template <typename T> 
class CaffeineBeverage  //咖啡因饮料
{
public:
	void PrepareRecipe() //咖啡因饮料冲泡法
	{
		BoilWater();  //把水煮沸
		Brew();    //冲泡
		PourInCup();  //把咖啡因饮料倒进杯子
		AddCondiments(); //加调料
	}
	void BoilWater()
	{
		cout << "把水煮沸" << endl;
	}
	void Brew()
	{
		static_cast<T *>(this)->Brew();
	}
	void PourInCup()
	{
		cout << "把咖啡倒进杯子" << endl;
	}
	void AddCondiments()
	{
		static_cast<T *>(this)->AddCondiments();
	}
};
class Coffee : public CaffeineBeverage<Coffee>
{
public:
	void Brew()
	{
		cout << "用沸水冲泡咖啡" << endl;
	}
	void AddCondiments()
	{
		cout << "加糖和牛奶" << endl;
	}
};
class Tea : public CaffeineBeverage<Tea>

{
public:
	void Brew()
	{
	cout << "用沸水浸泡茶叶" << endl;
	}
	void AddCondiments()
	{
		cout << "加柠檬" <<endl;
	}
};
int main(void)
{
	cout << "冲杯咖啡:" << endl;
	Coffee c;
	c.PrepareRecipe();
	cout << endl;
	cout << "冲杯茶:" << endl;
	Tea t;
	t.PrepareRecipe();
	return 0;
}

上段代码来点小插曲:就是this指针和模板中二段式名字查找。

this指针的用法: 
1. this指针是一个隐含于每一个成员函数中的特殊指针。它指向正在被该成员函数操作的那个对象
2. 当对一个对象调用成员函数时,编译程序先将对象的地址赋给this指针,然后调用成员函数,每次成员函数存取数据成员时,由隐含使用this指针。
3. 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
4. 在C++中,this指针被隐含地声明为: X *const this,这意味着不能给this 指针赋值;
   在X类的const成员函数中,this指针的类型为:const X* const, 这说明this指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);
5. 由于this并不是一个常规变量,所以,不能取得this的地址。

#include"stdafx.h"
#include<string>
#include <iostream>
using namespace std;
class A
{
public:
	void a()
	{
		cout << "A::a()" << endl;
	}
};
class B :public A
{
public:
	B(int q){ cout <<"输出初始化值"<<q<< endl; };
	int b()
	{
		a();
		this->a();
		cout << "B::b()"<< endl;
		return 0;
	}
private:
	int q;
};
int main(void)
{
	B *d=new B(1);
	d->a();
	d->b();
	return 0;
}

上面代码如果把class B的方法b()改为a(),并且去掉里面的a(),如果可以运行的话就是this指针只调用当先b(),不会调用A中的a()。但是会出现溢出,所以改了一下,验证this指针的调用问题。

第二个就是二段式名字查找,这是出现在类模板中的问题。

先看看编译器要做的三件事:

1 、名字查找 。

先在所在编译单元中可见名字实体中进行名字查找 。

(1) 类成员函数优先 ( 对象所在的类 -》 基类 )。 一经找到就停止查找 。

(2 )如果没有 ,在相应的名字空间中做进一步的搜索 ;

(3) 如果还没有 , 会根据函数参数所在的名字空间中查找 (keoning 查找 )。

2 、重载决议 。 根据所找到的名字进行重载决议 , 根据参数最匹配原则选择相应的函数 。

3、可访问性检查 。 用以确定被选中的函数是否可被调用 。

说明 :

1) 根据第一条 , 显然 , 如果类型想和非成员函数一起工作 , 那么它们应该放在同一个名字空间中 。 比如 , 一般类型的重载运算符和参数类型放在同一个头文件中/或者同一个名字空间下 。

2) 函数特化模板不参与重载决议 , 因此 , 如果想运用某个函数的特化 , 最好的方法是重载该函数 , 在实现中采用该特化来工作 。

3) 重载决议发生在可访问性检查之前 。 因此 , 如果私有函数不幸参与了重载 , 并且被选中 , 最终也会出现无法访问的编译提示 。 这常常隐含二义性 , 这样的设计本身也不合理 。 换句话说 , 私有参数 , 在名字查找和重载时并非是 ” 私有的 ”。

下面内容是来自here

问题起源

先看下面很简单的一小段程序。

#include <iostream>
template <typename T>
struct Base 
{
   void fun() 
   {
       std::cout << "Base::fun" << std::endl;
   }
};

template <typename T>
struct Derived : Base<T>
{
   void gun() 
  {
       std::cout << "Derived::gun" << std::endl;
       fun();
   }
};

这段代码在 GCC 下很意外地编译不过,原因竟然是找不到 fun 的定义,可是明明就定义在基类中了好吗!为什么视而不见呢?显然这和编译器对名字的查找方式有关,那这里面究竟有什么玄机呢?上述代码是写得不规范,还是 GCC 竟然存在这样愚蠢而又莫名其妙的 bug?

C++ 标准的要求

对于模板中引用的符号,C++ 的标准有这样的要求:

  1. 如果名字不依赖于模板中的模板参数,则该符号必须定义在当前模板可见的上下文内。

  2. 如果名字是依赖于模板中的模板参数,则该符号是在实例化该模板时,才对该符号进行查找。

也就是说,对于前面提到的例子,gun() 函数中调用 fun(),由于该 fun() 并不依赖于 Derived 的模板参数T,因此在编译器看来该调用就相当于 ::fun(),直接把它当成是一个外部的符号去查找,而此时外部又没有定义该函数,因此就报错了。要去除这种错误,解决的方法很简单,只要在调用 fun 的地方,人为地加上该调用对模板参数的依赖则可。

template <typename T>
struct Derived : Base<T>
{
   void gun() 
   {
       std::cout << "Derived::gun" << std::endl;
       this->fun();// or Base<T>::fun();
   }
};

关键点在于:不加 this,编译器就会把 fun 当作一个非成员函数,因此才报的错。确实是「非」成员函数。

这事得从模板的「二段式名字查找」(Two-Phase Name Lookup)说起。

根据 C++ 标准,对模板代码中的名字的查找,分为两个阶段进行:
  1. 模板定义阶段:刚被定义时,只有模板中独立的名字(可以理解为和模板参数无关的名字)参加查找
  2. 模板实例化阶段:实例化模板代码时,非独立的名字才参加查找。
如果没有用模板,事情会简单很多。然而这里的Derived本身是模板,需要进行二段式名字查找。
首先进入Derived的模板定义阶段,此时Derived的基类 Base<T> 依赖于模板参数 T,所以是一个「非独立」的名字。所以在这个阶段,对于Derived来说 Base<T> 这个名字是不存在的,于是 Base<T>::fun() 也不存在。但此时这段代码仍旧是合法的,因为此时编译器可以认为 fun 是一个非成员函数。
当稍晚些时候进入Derived的模板实例化阶段时,编译器已经坚持认为 f 是非成员函数,纵使此时已经可以查到 Base<T>::fun(),编译器也不会去这么做。
「查非成员函数为什么要去基类里面查呢?」于是就找不到了。
那我们回过头来看 this->fun():
  • 模板定义阶段:尽管没法查到 Base<T>::fun(),但明晃晃的 this-> 告诉编译器,fun是一个成员函数,不是在Derived类里,就是在Derived类的基类里,于是编译器记住了
  • 模板实例化阶段:此时编译器查找的对象是一个「成员函数」,首先在Derived中查,没有找到;然后在其基类里查,于是成功找到 Base<T>::fun()。

加上 this 之后,fun 就依赖于当前 Derived 类,也就间接依赖了模板参数T,因此名字的查找就会被推迟到该类被实例化时才去基类中查找。

两阶段名字查找

从前面的介绍,我们可以看到编译器对模板中引用的符号的查找是分为两个阶段的:

  1. 符号不依赖于当前模板参数,该符号则被当作是外部的符号,直接在当前模板所在的域中去查找。

  2. 符号如依赖于当前模板参数,则对该符号的查找被推迟到模板被实例化时。

为什么要这样区别对待呢?原因其实很简单,编译器在看到模板 Derived 的定义时,还不能确定它的基类最后是怎样的:Base 有可能会在后面被特化,使得最后被继承的具体基类中不一定还有 fun() 函数。

template <>
struct Base<int> 
{
   void fun2() 
   {
       std::cout << "Specialized, Base::fun2" << std::endl;
   }
};

因此编译器在看到模板类的定义时,还不能判断它的基类最后会被实例化成怎样,所以对依赖于模板参数的符号的查找只能推迟到该模板被实例化时才进行,而如果符号不依赖于模板参数,显然没有这个限制,因此可以在看到模板的定义时就直接进行查找,于是就出现了对不同符号的两阶段查找。

符号与类型问题

对于前面介绍中提到的符号,我们其实默认指的是变量,细心的读者可能会想到,在继承类中引用的符号,还可能会是类型,而由于模板特化的存在,在名字查找的第一阶段编译器也是没法判断出该符号最后到底是怎样的类型,甚至不能知道是不是一个类型。

template <typename T>
struct Base 
{
   typedef char* baseT;
};

template <typename T>
struct Derived : Base<T>
{
   void gun()
   {
      Base<T>::baseT p = "abc";
   }
};
template <>
struct Base<int>
{
   typedef int baseT;
};

template <>
struct Base<float>
{
   int baseT;
};

如上例子,Derived 中 gun() 函数对 Base::baseT 的引用会造成编译器的迷惑,它在看到 Derided 的定义时,根本无从知道 Base::baseT 究竟是一个变量名,还是一个类型,以及什么类型?而它又不能直接把这一部分相关的代码全部都推迟到第二阶段再进行,因此在这儿它就会报错了:它可以不知道这个类型最后是什么类型,但它必须知道它究竟是不是类型,如果连这个都不知道,接下来相关的代码它都没法去解析了。因此,实际上,编译器在看到一个与模板参数相关的符号时,默认它都是当作一个变量来处理的,所以在上述的例子中,编译器在看到 Derived 的定义时,它直接把 Base::baseT 当成了一个变量来处理,所以就会报错了。

那么,我们要怎样才能让编译器知道其实 Base::baseT 是一个类型呢? 必须得显式地告诉它,因此需要在引用 Base::baseT 时,显式地加入一个关键字:typename.

template <typename T>
struct Derived : Base<T>
{
   void gun()
   {
      typename Base<T>::baseT p = "abc";
   }
};

此时,编译器看到有 typename 显式地指明 baseT 是一个类型,它就不会再把它默认当成是一个变量了,从而使得名字查找的第一个阶段可以继续下去。

总结

模板中名字的查找会因为该名字是否依赖于模板参数而有所不同。

依赖于模板参数的名字(如函数的参数的类型是模板的参数),其符号解析会在第二阶段进行,其查找方式有两个:

  1. 在模板定义的域内可见的符号。(很严格)
  2. 在实例化模板的域内通过 ADL 的方式查找符号。(也很严格,杜绝了不同 namespace 内部重复定义导致冲突的问题)。

而不依赖于模板参数的符号,则只会在定义模板的可见域内进行查找,语言的定义严格如上所述,但实际编译器的支持上,msvc 不支持两阶段的查找(vc 2010 以前),gcc 的实现在 4.7 以前也不完全符合标准,一个比较全面的符合规范的例子,请参看如下:

void f(char); // 第一个 f 函数
 
template<class T> 
void g(T t) {
    f(1);    // 不依赖参数的符号,符号解释在第一阶段进行,找到 ::f(char)
    f(T(1)); // 依赖参数的符号: 查找推迟
    f(t);    // 依赖参数的符号: 查找推迟
}
 
enum E { e };
void f(E);   // 第二个 f 函数
void f(int); // 第三个 f 函数
 
void h() {
    g(32); // 实例化 g<int>, 此时进行查找 f(T(1)) 和 f(t)
           // f(t) 的查找找到 f(char),因为是通过非 ADL 方式查找的(T 是 int,ADL 失效),而定义模板的域内只有 f(char)。
           // 同理,f(T(1)) 的查找也只找到 f(char)。

    g(e); // 实例化 g<E>, 此时进行查找 f(T(1)) 和 f(t),因为参数都是用户定义的类型,ADL 起效,因此两者均找到了 f(E),

}
 
typedef double A;
template<class T> class B {
   typedef int A;
};

template<class T> struct X : B<T> {
   A a; // 此处 A 为 double
};




评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值