您的位置:首页 > 编程语言 > C语言/C++

深度探索c++对象模型之template的具现行为

2016-12-02 18:26 302 查看
      让我们先来设计一个模板类,名字叫Point:
template <class Type>
class Point
{
public:
enum Status{ unallocated, normalized };

Point( Type x=0.0, Type y=0.0, Type z=0.0 );
~Point();

void* operator new( size_t );
void  operator delete(void* , size_t);
...
private:
static Point<Type> *freelist;
static int chunksize;
Type _x,_y,_z;
}

      首先,当编译器看到Point模板的声明时,它会一点反应都没有!也就是说,上述的静态数据成员并不可用,nested enum和enumerator也一样。而且虽然enum Status的真正类型和它的enumerator在所有的Point模板类中都一样,但它们只能通过对Point模板类的某个实体具现后来存取或操作。因此我们可以这样写:

Point<float>::Status s; //合法的但不能这样写:
Point::Status s; //非法的!即使这两种方式在实际意义上来说是一样的。(在理想情况下,我们希望这个enum只有一个实体被产生出来。如果不是这样,我们会想要把这个enum抽出一个nontemplate bass class中,以避免多份拷贝)。

      同理,freelist和chunksize也是不可用的:
Point::freelist = ...; //非法的!
我们应该写成这样:
Point<float>::freelist = ...; //合法的像上面这样使用static date member,会使它的一份实体与Point模板类的float instantiation【浮点具现】方式在程序中产生关联。如果我们这样写:
Point<double>::freelist = ...;就会产生第二个freelist实体【像非模板类那种,静态数据成员只能有一个,而这个Point却可以不止一个】,与Point模板类的double instantiation方式在程序中产生关联。
      那么如果我们定义个指针,指向一个特别实体NULL:

Point<float> *ptr = 0;那么在程序中会发生什么呢?依然是什么都没发生!因为一个指向类对象的指针,本身并不是一个类对象,编译器不需要知道与该类有关的任何members或object布局概况,所以将上面那个Point的一个float实体具现也就没有意义。在c++ standard完成之前,“声明一个指针指向某个template class”这种事并没有被强制定义,编译器可以自行决定要不要把template具现出来。但如今的C++ standard禁止这么做了!
      那么如果不是pointer而是reference呢:

const Point<float> &ref = 0;这一次,编译器真的会具现出一个Point的float实体来。上面的例子会被扩展为:
//编译器处理后
Point<float> temporary( float( 0 ) );
const Point<float> &ref = temporary;这是因为reference并不是无物性质:0并不会视为NULL,而是被当作数据,必须被转换为以下类型的一个对象:
Point<float>如果没有转换的可能,那么这个定义就会被编译器报错。
      所以,一个class object的定义,不论是编译器暗中的做,还是由用户像下面这样做:

const Point<float> origin;都会导致template class的具现,既float instantiation的真正对象布局会被产生出来,在Point模板类里面有三个非静态数据成员,每一个类型都是可变换的Type,现在Type被绑定为float,所以origin的配置空间至少要能够容纳三个float成员。
      接下来就要谈到成员函数的实体化规则了:只有当成员函数在程序中被使用的时候,C++ standard才要求它们被具现出来,之所以由用户主导instantiation具现规则,因为两个原因:

1):空间和效率的考虑,比如一个class中有100个成员函数,但实际代码中只针对某个类型使用其中一两个,针对另一个类型使用其中的三四个,无疑我们将那100个成员函数全部具现会浪费大量空间和时间。

2):尚未实现的机能:并不是一个template具现出来的所有类型就一定能够支持成员函数所需要的所有运算符,比如我们有一个T模板类,用void*具现了,在T模板类里面有一个成员函数,该成员函数里面用到了求余运算符%,而指针与%搭配,会生出什么鬼?所以如果只具现那些真正用到的成员函数,编译器就能支持那些原本会造成编译错误却又不得不写的成员函数。

      举个例子,origin的定义需要调用Point的构造函数和析构函数,那么上面的例子中只有这两个函数被具现了。类似的道理,如果我们写:

Point<float> *p = new Point<float>;那么被具现的分别是:1),Point模板类的float实体;2),Point模板类的new运算符成员函数;3),Point模板类的构造成员函数。有趣的是,虽然new运算符是这个模板类的implicitly static member【隐式静态成员】,以至于它不能够处理任何的非静态成员,但它还是依赖真正的template参数类型,因为它的参数size_t代表class的大小。
      那么这些函数在什么时候具现出来呢?当前【指作者写此书的时候】流行两种策略:

1):在编译时候,这些函数将被具现于origin和p存在的那个文件里。

2):在链接时候,编译器会被一些辅助工具重新激活,具现出来的template函数实体可能会被放在某个文件中,或一个分离的存储位置中。

      最后说一个有趣的现象,在int和long一样的结构【指操作系统,int和long是一样字节长度】中,如下的具现:

Point<int> i;
Point<long> l;会产生一个还是两个实体呢?目前我【指作者】所知道的所有编译器都是两个实体,具现出两组成员函数,而C++ standard并没有对此有强制规定。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息