More Effective C++: 05技术(25-28)

25:将constructor 和 non-member functions 虚化

         所谓 virtual constructor是某种函数,视其输入可产生不同类型的对象。比如下面的代码:

class NLComponent {
public:
    ...
};

class TextBlock: public NLComponent {
public:
    ...
};

class Graphic: public NLComponent {
public:
    ...
};

class NewsLetter {
public:
    NLComponent *readComponent(std::string &str);
...
private:
list<NLComponent*> components;
};

 readComponent根据参数str,决定产生TextBlock或Graphic。由于它产生新的对象,所以行为彷若constructor,但由于它能够产生不同型别的对象,所以称它为一个virtual constructor。

有一种特别的virtual constructor,称为virtual copy constructor,它会传回一个指针,指向其调用者(某对象)的一个新副本。比如下面的clone函数:

class NLComponent {
public:
    virtual NLComponent * clone() const = 0;
    ...
};

class TextBlock: public NLComponent {
public:
    virtual TextBlock * clone() const
    { return new TextBlock(*this); }
    ...
};

class Graphic: public NLComponent {
public:
    virtual Graphic * clone() const
    { return new Graphic(*this); }
    ...
};

          注意,当 derived class 重新定义其base class 的一个虚函数时,如果函数的返回类型是个指针或引用),指向一个base class,那么derived class的函数可以返回一个指针或引用,指向该base class的一个derived class。

 

         所谓将non-member functions 虚化,也就是让non-member functions的行为视其参数的动态类型而不同。比如下面的代码:

class NLComponent {
public:
    virtual ostream& print(ostream& s) const = 0;
    ...
};

class TextBlock: public NLComponent {
public:
    virtual ostream& print(ostream& s) const;
    ...
};

class Graphic: public NLComponent {
public:
    virtual ostream& print(ostream& s) const;
    ...
};

inline ostream& operator<<(ostream& s, const NLComponent& c)
{
    return c.print(s);
}

          上面的operator<<就相当于一个虚化的non-member函数。注意,不能将operator<<定义为成员函数,这是因为operator<<的第一个参数必须是ostream变量。

 

 

26:限制某个class所能产生的对象数量

         1;允许0或1个对象

         每当即将产生一个对象,我们确知一件事情:会有一个构造函数被调用。因此,阻止某个class产出对象的最简单方法就是将其构造函数声明为private。

如果想要限制只能产生一个对象,则可以这样:

class PrintJob;
class Printer {
public:
    void submitJob(const PrintJob& job);
    void reset();
    void performSelfTest();
    ...
    friend Printer& thePrinter();
private:
    Printer();
    Printer(const Printer& rhs);
    ...
};

Printer& thePrinter()
{
    static Printer p; //唯一的一个打印机对象
    return p;
}

string buffer;
thePrinter().reset();
thePrinter().submitJob(buffer);

 这样的设计有3个成分:一,Printer class的constructors为private,可以抑制对象的产生;二,全局函数thePrinter被声明为此class的一个friend,致使thePrinter不受private  constructors 的约束;三,thePrinter内含一个 static Printer对象,意思是只有一个Printer对象会被产生出来。

也可以将thePrinter声明为Printer的一个static member函数:

class Printer {
public:
    static Printer& thePrinter();
    ...
private:
    Printer();
    Printer(const Printer& rhs);
    ...
};

Printer& Printer::thePrinter()
{
    static Printer p;
    return p;
}

 以上两个thePrinter函数的实现,有个精细的地方值得讨论:形成唯一一个Printer对象的,是函数中的static对象,而非class中static对象。这点很重要,class拥有一个static对象的意思是,纵使从未被用到,它也会被建构(及解构)。而函数中的static对象,只有在函数第一次被调用时才会产生。另外,函数内的static对象的初始化时机是确定的(第一次调用),而class的static对象则不一定在什么时候初始化。C++对于同一编译单元内的static对象的初始化次序是有提出一些保证的,但对于不同编译单元内的static对象的初始化次序没有任何说明。

 

或许你认为更好的作法是简单地计算目前存在的对象个数,当外界创建太多对象时,在    构造函数内丢出一个异常。比如下面的代码:

class Printer {
public:
    class TooManyObjects{}; // 当创建的对象过多时就使用这个异常类
    Printer();
    ~Printer();
    ...
private:
    static size_t numObjects;
    Printer(const Printer& rhs); // 这里只能有一个printer,所以不允许拷贝
};

size_t Printer::numObjects = 0;
Printer::Printer()
{
    if (numObjects >= 1) {
        throw TooManyObjects();
    }
    继续运行正常的构造函数;
    ++numObjects;
}

Printer::~Printer()
{
    进行正常的析构函数处理;
    --numObjects;
}

 这种限制对象数量的方法更直接易懂,而且很容易被一般化,使对象的最大数量可以设定为1以外的值。

但是这种策略也有问题。当Printer被继承,或是被包含于其他类内部时,继承类或包含类的数目也被限制住了:

class ColorPrinter: public Printer {
    ...
};
Printer p;
ColorPrinter cp;

 这里实际上创建了两个Printer对象,一个是 p,另一个是cp内的“Printer 成份”。一旦执行,在cp的“base class部分”建构时,会有一个TooManyObjects异常被抛出。

class CPFMachine {
private:
    Printer p;
    ...
};
CPFMachine m1;
CPFMachine m2;

 当创建m2时,也会抛出一个TooManyObjects异常。

问题原因在于:Printer对象以三种不同的状态而存在:它自己;继承类的“base class部分”;内嵌于其他对象之中。这些不同状态的呈现,把“追踪目前存在的对象个数”的意义严重弄混了。你心里头所想的“目前存在的对象个数”可能和编译器所想的不同。

如果采用原先的策略的话,因为Printer构造函数是private,且如果没有声明任何friend的话,则带有private constructors的类一旦被继承,或被内嵌于其他类内,则继承类或包含类不能在创建对象。

 

还有一点值得注意:使用thePrinter函数封装对单个对象的访问,以便把Printer对象的数量限制为一个,这样做的同时也会使每一次运行程序时只能使用一个Printer对象。导致我们不能这样编写代码:

建立 Printer 对象 p1;
使用 p1;
释放 p1;
建立Printer对象p2;
使用 p2;
释放 p2;

 这种设计在同一时间里没有实例化多个Printer对象,而是在程序的不同时间使用了不同的Printer对象。不允许这样编写有些不合理。毕竟我们没有违反只能存在一个printer的约束。下面的使这种方式成为可能

 

当然有。可以把先前使用的对象计数的代码与刚才看到的伪构造函数代码合并在一起:

class Printer {
public:
    class TooManyObjects{};
    static Printer * makePrinter();    // 伪构造函数
    ~Printer();
    void submitJob(const PrintJob& job);
    void reset();
    void performSelfTest();
    ...
private:
    static size_t numObjects;
    Printer();
    Printer(const Printer& rhs);
};

size_t Printer::numObjects = 0;
Printer::Printer()
{
    if (numObjects >= 1) {
        throw TooManyObjects();
    }
    继续运行正常的构造函数;
    ++numObjects;
}

Printer * Printer::makePrinter()
{ return new Printer; }

 除了用户必须调用伪构造函数,而不是真正的构造函数之外,它们使用Printer类就像使用其他类一样:

Printer p1; // 错误! 缺省构造函数是private
Printer *p2 = Printer::makePrinter(); // 正确, 间接调用缺省构造函数
Printer p3 = *p2; // 错误! 拷贝构造函数是private
p2->performSelfTest(); // 所有其它的函数都可以正常调用
p2->reset(); 
...
delete p2; 

 

这种技术很容易推广到限制对象为任何数量上。只需把numObjects常量值1改为其他值,然后消除拷贝对象的约束即可:

class Printer {
public:
    class TooManyObjects{};
    static Printer * makePrinter(); // 伪构造函数
    static Printer * makePrinter(const Printer& rhs); //伪复制构造函数
    ...
private:
    static size_t numObjects;
    static const size_t maxObjects = 10;
    Printer();
    Printer(const Printer& rhs);
};

size_t Printer::numObjects = 0;
Printer::Printer()
{
    if (numObjects >= maxObjects) {
        throw TooManyObjects();
    }
    ...
}
Printer::Printer(const Printer& rhs)
{
    if (numObjects >= maxObjects) {
        throw TooManyObjects();
    }
    ...
}

Printer * Printer::makePrinter()
{ return new Printer; }
Printer * Printer::makePrinter(const Printer& rhs)
{ return new Printer(rhs); }

  

2:一个具有对象计数功能的基类

如果我们有大量像Printer这样需要限制对象数量的类,此时就需要编写一个具有对象计数功能的基类,然后让像Printer这样的类从该基类继承。

在基类中封装全部的计数功能,包括静态变量numObjects,为了确保每个进行实例计数的类都有一个相互隔离的计数器,可以使用计数类模板:

template<class BeingCounted>
class Counted {
    public:
    class TooManyObjects{}; // 用来抛出异常
    static int objectCount() { return numObjects; }
protected:
    Counted();
    Counted(const Counted& rhs);
    ~Counted() { --numObjects; }
private:
    static int numObjects;
    static const size_t maxObjects;
    void init();
};

template<class BeingCounted>
Counted<BeingCounted>::Counted()
{ init(); }
template<class BeingCounted>
Counted<BeingCounted>::Counted(const Counted<BeingCounted>&)
{ init(); }
template<class BeingCounted>

void Counted<BeingCounted>::init()
{
    if (numObjects >= maxObjects) throw TooManyObjects();
    ++numObjects;
}

 这个模板生成的类仅能被做为基类使用,因此构造函数和析构函数被声明为protected。现在我们能修改Printer类,这样使用Counted模板:

class Printer: private Counted<Printer> {
public:
    static Printer * makePrinter();// 伪构造函数
    static Printer * makePrinter(const Printer& rhs);
    ~Printer();
    void submitJob(const PrintJob& job);
    void reset();
    void performSelfTest();
    ...
    using Counted<Printer>::objectCount; // 参见下面解释
    using Counted<Printer>::TooManyObjects; // 参见下面解释
private:
    Printer();
    Printer(const Printer& rhs); bbs.theithome.com
};

Printer::Printer()
{
    进行正常的构造函数运行
}

 Printer类private继承Counter模板,因为它们之间是implemented-in-terms-of的关系。

Counted所做的大部分工作对于Printer的用户来说都是隐藏的,但是这些用户可能很想知道有当前多少Printer对象存在。Counted模板提供了objectCount函数,用来提供这种信息,但是因为我们使用private继承,这个函数在Printer类中成为了private。为了恢复该函数的public访问权,我们使用using声明。TooManyObjects类也用同样的方式来处理,因为Printer的客户端如果要捕获这种异常类型,它们必须有能力访问TooManyObjects。

当Printer继承Counted<Printer>时,它可以忘记有关对象计数的事情。编写Printer类时根本不用考虑对象计数。

 

最后还有一点需要注意,必须定义Counted内的静态成员。对于numObjects来说,只需要在Counted的实现文件里定义它即可:

template<class BeingCounted> // 定义numObjects
int Counted<BeingCounted>::numObjects; // 自动把它初始化为0

 对于maxObjects来说,则有一些技巧。有的类可能需要限制对象数量为10,有的则为16。这种情况下,我们不对maxObject进行初始化。而是让此类的客户端提供合适的初始化。比如Printer的作者必须把这条语句加入到一个实现文件里:

template<> 
const size_t Counted<Printer>::maxObjects = 2;

 如果这些作者忘了对maxObjects进行初始化,连接时会发生错误:” undefined reference to `Counted<Printer>::maxObjects'”。

 

 

27:要求(或禁止)对象产生于heap之中

         1:要求对象产生于heap之中

         为了限制对象必须产生于heap之中,需要阻止用户不得使用new以外的方法产生对象。non-heap objects会在其定义点自动建构,并在其寿命结束时自动解构,所以只要让那些被隐秘调用的构造动作和析构动作不合法就可以了。最直接的方式就是将构造函数和析构函数都声明为private,但这实在太过了,比较好的办法是让析构函数成为private而构造函数仍为public:

class UPNumber {
public:
    UPNumber();
    UPNumber(int initValue);
    UPNumber(double initValue);
    UPNumber(const UPNumber& rhs);
    // 伪析构函数。这是一个 const member function,
    // 因为 const  对象也可能需要被摧毁。
    void destroy() const { delete this; }
...
private:
    ~UPNumber();  
};

UPNumber n; // 编译错误!
UPNumber *p = new UPNumber; //  良好。
...
delete p; // 编译错误!
p->destroy(); //  良好。

          将析构函数声明为private之后,”UPNumber n;”和”delete p;”都会报编译错误:‘UPNumber::~UPNumber()’ is private, within this context UPNumber n(或delete p);

         但是,就像之前提到过的,这种方法也阻止了继承和包含:UPNumber无法被继承,也无法被其他类包含。单纯声明继承或包含UPNumber的类虽然可以通过编译,但是却无法使用,不能产生这种类的对象。这些困难都可以克服。令UPNumber的析构函数成为   protected(并仍保持其构造函数为public),便可解决继承问题;将“内含UPNumber对象”的类,改为“包含一个指针,指向 UPNumber对象”,可解决包含问题。

        

         2:判断某个对象是否位于Heap内

         将析构函数声明为protected,可以解决继承问题,使得”NonNegativeUPNumber n;”这样的语句能编译通过。但是,在n中,基类UPNumber的部分实际上在栈中而不是heap中。如果需要限制住所有UPNumber对象(包括继承类对象中的UPNumber部分)都必须在堆中,怎么办?

         实际上没有简单有效的办法,因为UPNumber构造函数不可能知道它被调用是否是为了产生某个heap中继承类对象的基类部分。或许你认为下面使用operator new的方法能够解决这个问题:

class UPNumber {
public:
    class HeapConstraintViolation {}; //如果产生一个非heap对象,就丢出该异常
    static void * operator new(size_t size);
    UPNumber();
    ...
private:
    static bool onTheHeap;  
...
};

bool UPNumber::onTheHeap = false;
void *UPNumber::operator new(size_t size)
{
    onTheHeap = true;
    return ::operator new(size);
}

UPNumber::UPNumber()
{
    if (!onTheHeap) {
        throw HeapConstraintViolation();
    }
    proceed with normal construction here;
    onTheHeap = false; // 清除 flag,供下一个对象使用。
}

          operator new将onTheHeap置为true,而每一个构造函数都会检查onTheHeap的值,判断建构中的对象的内存是否由operator new 配置而来。如果不是,就丢出一个异常,否则构造函数就如常继续下去。一旦建构完成,onTheHeap会被设为false,为下一个将被建构的对象重新设定好默认值。    这种方法有几个问题:

像” UPNumber *numberArray = new UPNumber[100];”这样的语句,即使在operator new[]中做类似的配置,但是这里分配内存的动作只有一次,而构造函数却需要被调用100次,在第二次调用构造函数时,就会抛出一个异常;

像”UPNumber *pn = new UPNumber(*new UPNumber);”这样的语句(先忽略内存泄漏),编译器可能产生的动作是:为第一个对象调用operator new,为第二个对象调用operator new,为第一个对象调用构造函数,为第二个对象调用构造函数。这时的问题跟上面的数组的问题类似。

 

为了能让构造函数判断*this是否位于heap中,你或许会采用一种“不可移植”的方法。许多操作系统中,程序的地址空间,stack地址空间在高地址处,地址从高地址往低地址成长,heap地址空间在低地址处,地址由低地址往高地址成长。你觉得可以利用下面的函数判断某个地址是否位于heap中:

bool onHeap(const void *address){
    char onTheStack;
    return address < &onTheStack;
}

          在onHeap函数内,onTheStack是个局部变量。它被置于stack内。当onHeap被调用,其stack  frame会被放在stack的最顶端,由于此架构中的stack向低地址成长,所以onTheStack的地址一定比其他任何一个位于stack 中的变量(或对象)更低。因此,如果参数address比onTheStack的地址更低,它就不可能位于stack,那就一定是位于heap。

         这种方法的问题在于,除了栈和堆,还有静态对象的地址空间,它通常位于堆之下,因此,上面的函数无法区分堆对象和静态对象。因此,为了判断某个地址是否位于heap中,一定会走入不可移植的,视系统而异的阴暗角落。所以你最好重新设计你的软件,避免需要判断某个对象是否位于heap内。

        

         判断地址是否存在于heap中是困难的,但是判断某个地址被delete是否安全却相对简单些。只要地址是new出来的,delete它就是安全的。可以设计一个abstract mixin base class,用于为派生类提供“判断某地址是否是operator new出来的”的能力:

class HeapTracked {
public:
    class MissingAddress{};
    virtual ~HeapTracked() = 0;
    static void *operator new(size_t size);
    static void operator delete(void *ptr);
    bool isOnHeap() const;
private:
    typedef const void* RawAddress;
    static list<RawAddress> addresses;
};

list<RawAddress> HeapTracked::addresses;
HeapTracked::~HeapTracked() {}

void * HeapTracked::operator new(size_t size)
{
    void *memPtr = ::operator new(size); 
    addresses.push_front(memPtr);
    return memPtr;
}

void HeapTracked::operator delete(void *ptr)
{
    list<RawAddress>::iterator it = find(addresses.begin(), addresses.end(), ptr);
    if (it != addresses.end()) {
        addresses.erase(it);
        ::operator delete(ptr);
    } else { //表示ptr不是operator new所返回
        throw MissingAddress();
    }
}

bool HeapTracked::isOnHeap() const
{
    const void *rawAddress = dynamic_cast<const void*>(this);
    list<RawAddress>::iterator it = find(addresses.begin(), addresses.end(), rawAddress);
    return it != addresses.end();
}

          需要注意的是下面的语句:const void *rawAddress = dynamic_cast<const void*>(this);

这是因为凡涉及“多重或虚拟基类”的对象,会拥有多个地址,因为isOnHeap只施行于HeapTracked对象身上,我们可以利用dynamic_cast的特殊性质来消除这个问题。只要简单地将指针“动态转型”为void*(或const void* 或 volatile void* 或 const volatile void*),便会获得一个指针,指向“普通指针所指对象”的内存起始处。不过,dynamic_cast只适用于“所指对象至少有一个虚函数”的指针身上,这对于HeapTracked及其子类而言不成问题。

         定义了该抽象基类之后,就可以为任何类加上“追踪指针”的能力:

class Asset: public HeapTracked {
private:
    UPNumber value;
    ...
};

void inventoryAsset(const Asset *ap)
{
    if (ap->isOnHeap()) {
        ap is a heap-based asset — inventory it as such;
    }
    else {
        ap is a non-heap-based asset — record it that way;
    }
}

 

         3:禁止对象产生于Heap内

         如前所述,对象的存在有三种可能:直接被构造;被构造为派生类对象中的基类部分;内嵌于其他类对象之中。

         为了防止对象被直接构造在heap中,可以在类中声明operator new为private:

class UPNumber {
private:
    static void *operator new(size_t size);
    static void operator delete(void *ptr);
    ...
};

UPNumber n1; //  可以
static UPNumber n2; //  可以
UPNumber *p = new UPNumber; // 错误!企图调用private operator new

          如果也想禁止“由UPNumber对象所组成的数组”位于heap内,可以将operator new[]  和operator delete[]也声明为private。

将operator new声明为private,往往也会阻止UPNumber对象被构建为heap-based 派生类对象的基类部分。那是因为operator new和operator delete都会被继承,所以如果这些函数不在派生类内声明为public,派生类便继承其基类中的版本:

class NonNegativeUPNumber:public UPNumber {
    ...
};
NonNegativeUPNumber n1; //可以
static NonNegativeUPNumber n2; //可以
NonNegativeUPNumber *p = new NonNegativeUPNumber; //错误

 但是如果派生类声明了一个属于自己的operator new,且为public,则该派生类可被构建与heap中。因此需要另觅良法以求阻止“UPNumber作为基类部分”被构建。类似的,当企图构建一个“包含UPNumber对象”的对象时,“UPNumber的operator new为private”这一事实并不会带来什么影响:

class Asset {
public:
    Asset(int initValue);
    ...
private:
    UPNumber value;
};
Asset *pa = new Asset(100); //没问题,调用的是Asset::operator new
                            //或::operator new,而非UPNumber::operator new

 这就又回到了之前的问题:没有任何可移植的作法用以判断某地址是否位于heap内一样,我们也没有可移植性的方法可以判断它是否不在heap 内。

 

 

28:智能指针

         智能指针的operator*的伪代码如下:

template<class T>
T& SmartPtr<T>::operator*() const
{
    perform "smart pointer" processing;
    return *pointee;
}

 注意,返回的是引用。如果返回的是对象,结果会惨不忍睹。首先,pointee不一定指向类型为T的对象,也有可能指向T派生类的对象,若真如此返回对象就会发生截断;而且也不能支持像*p=3这样的操作。

 

         为了使智能指针能像普通指针那样,方便的判断它是否为null:

SmartPtr<TreeNode> ptn;
if (ptn == 0) ...
if (ptn) ...
if (!ptn) ...

          需要提供一种隐式类型转换,传统做法是转换为void*:

template<class T>
class SmartPtr {
public:
    ...
    operator void*();//如果smart ptr是null,传回零,否则传回非零值。
};

 但是就像所有隐式类型转换一样,此处这个也有相同的缺点:在程序员认为“函式调用失败”的情况下,编译器却让它成功:

SmartPtr<Apple> pa;
SmartPtr<Orange> po;
if (pa == po) ... //竟然可以过关

 以“转换至void*”为基调,可衍生出各种变形。某些设计者喜欢转换为const void*,另一些人喜欢转换为 bool,但没有一个可以消除上述问题。

 

除非万不得已,不要提供对普通指针的隐式转换运算符。智能指针如果提供隐式转换至   普通指针,便是打开了一个难缠BUG的门户。比如:

DBPtr<Tuple> pt = new Tuple;
...
delete pt;

 这应该无法编译,毕竟 pt是对象而非指针,delete只能操作指针。编译器会寻找隐式类型转换,尽可能让函数调用成功。delete会调用析构函数和operator delete。所以在上述的delete语句中,它暗自将pt 转换为一个Tuple*,然后删除。这几乎一定会弄糟你的程序。该对象现在被删除了两次,一次是在delete调用时,一次是在pt 的析构函数被调用时(离开作用于自动析构)。将对象删除一次以上会导至未定义的行为。

 

智能指针之间的转换,可以通过成员函数模板(member function template)来实现,通过这种技术,如果有个普通指针类型为T1*,另一个普通指针类型为T2*,只要能够将T1*隐式转换为T2*,便能够将smart pointer-to-T1 隐式转换为 smart pointer-to-T2:

class MusicProduct{};
class Cassette: public MusicProduct{};

template<class T>
class SmartPtr {
public:
    SmartPtr(T* realPtr = 0);

    template<class newType>
    operator SmartPtr<newType>()
    {
        return SmartPtr<newType>(pointee);
    }
...
private:
    T *pointee;
};

void displayAndPlay(const SmartPtr<MusicProduct>& pmp, int howMany);
SmartPtr<Cassette> funMusic(new Cassette("Alapalooza"));

displayAndPlay(funMusic, 10);

 funMusic对象属于SmartPtr<Cassette> 类型,而displayAndPlay函数期望得到的是个 SmartPtr<MusicProduct>对象。编译器侦测到类型不吻合,于是寻找某种方法,企图将 funMusic转换为一个SmartPtr<MusicProduct>对象:编译器在SmartPtr<MusicProduct>内企图寻找一个“单一自变量之constructor”,且其自变量类型为SmartPtr<Cassette>,但是没有找到;于是再接再励在SmartPtr<Cassette>内寻找一个隐式类型转换运算符,希望可以产出一个SmartPtr<MusicProduct>;也失败了,接下来编译器再试图寻找一个“可具现化以导出合适之转换函数”的member function template。这一次它们在SmartPtr<Cassette> 找到了这样一个东西,当它被具现化并令newType绑定至MusicProduct时,产生了所需函数。于是编译器将该函数具现化,导出以下函数码:

SmartPtr<Cassette>::operator SmartPtr<MusicProduct>()
{
    return SmartPtr<MusicProduct>(pointee);
}

 这里的问题是否可以以一个 Cassette*指针建构出一个 SmartPtr<MusicProduct>对象。SmartPtr<MusicProduct>构造函数期望获得一个MusicProduct*指针,很明显地Cassette*可被交给一个期望获得MusicProduct*的函数。因此SmartPtr<MusicProduct> 的构建会成功,而    SmartPtr<Cassette>至SmartPtr<MusicProduct>的转换也会成功。

 

这项技术也不是万能的。假设扩充MusicProduct继承体系,加上一个新的CasSingle类,使其继承Cassette。现在考虑这份代码:

template<class T>
class SmartPtr { ... };

void displayAndPlay(const SmartPtr<MusicProduct>& pmp, int howMany);
void displayAndPlay(const SmartPtr<Cassette>& pc, int howMany);
SmartPtr<CasSingle> dumbMusic(new CasSingle("Achy Breaky Heart"));
displayAndPlay(dumbMusic, 1); //编译错误!

 displayAndPlay被重载,一个接收SmartPtr<MusicProduct>对象,另一个接收SmartPtr<Cassette>对象。当我们调用displayAndPlay并给予一个SmartPtr<CasSingle>,我们预期调用的是SmartPtr<Cassette>函数,因为CasSingle直接继承自Cassette而间接继承自MusicProduct。如果面对的是普通指针,情况确实是这样的。

但只能指针没有那么机灵,它们把member functions拿来做为转换运算符使用,而C++的理念是,对任何转换函数的调用动作,都是等同视之。于是,displayAndPlay的调用动作成为一种有二义性的行为,因为从SmartPtr<CasSingle>转换至SmartPtr<Cassette>,并不比转换至SmartPtr<MusicProduct>更好。

 

普通指针与const有3中结合方式:

CD goodCD("Flood");
const CD *p;
CD * const p = &goodCD;
const CD * const p = &goodCD;

 智能指针也类似:

SmartPtr<CD> p;
SmartPtr<const CD> p;
const SmartPtr<CD> p = &goodCD;
const SmartPtr<const CD> p = &goodCD;  

 但是,普通指针可以将指向non-const对象的指针转换为指向const对象的指针,这对于智能指针却行不通:

CD *pCD = new CD("Famous Movie Themes");
const CD *pConstCD = pCD; //可以

SmartPtr<CD> pCD = new CD("Famous Movie Themes");
SmartPtr<const CD> pConstCD = pCD; //不可以

 这是因为SmartPtr<CD>和SmartPtr<constCD>是完全不同的类型。可以使用之前介绍的成员函数模板完成从SmartPtr<CD>到SmartPtr<const CD>的转换。

实际上还有另外一种方法实现这种转换:从non-const转换至const是安全的,从const转换至non-const则不安全。此外,能够对const指针所做的任何事情,也都可以对non-const指针进行,但你还可以对后者做更多事情。同样道理,你能够对pointer-to-const所做的任何事情,也都可以对pointer-to-non-const做,但是面对后者,你还可以另做其他事情。

这些规则听起来和public继承的规则很类似。因此,可以利用这种类似性质,令每一个smart pointer-to-T class公开继承一个对应的smart pointer-to-const-T class:

template<class T>
class SmartPtrToConst {
    ...
    protected:
    union {
        const T* constPointee;
        T* pointee;
    };
};

template<class T>
class SmartPtr:public SmartPtrToConst<T> {
    ...
};

 smart pointer-to-non-const-T对象必须内含一个原始的pointer-to-non-const-T指针,而smart pointer-to-const-T必须内含一个原始的pointer-to-const-T指针。一般的想法是放一个pointer-to-const-T于base class中,并放一个pointer-to-non-const-T于derived class中。然而这是一种浪费,因为SmartPtr对象会因此内含两个pointers:其一继承自SmartPtrToConst,其二是SmartPtr本身所有。

这个问题可通过union解决。union的访问级别应该是protected,使两个类都可取用。其中内含两个必要的原始pointer类型:constPointee指针供SmartPtrToConst<T>对象使用,pointee指针则供SmartPtr<T>对象使用。当然两个类的成员函数必须约束自己,只使用适当的指针。编译器无法协助厉行这项规范。运用这个新设计,我们获得了我们希望的行为:

template<class T>
class SmartPtrToConst {
public:
    SmartPtrToConst(const T*p):constPointee(p)
    {
        printf("this is SmartPtrToConst ctro, constPointee is %p, pointee is %p\n", constPointee, pointee);
    }
    void fun1() 
    {
        printf("in smartptrotconst, read const ptr: %d\n", *constPointee);
    }
protected:
    union{
        const T* constPointee;
        T* pointee;
    };
};

template<class T>
class SmartPtr : public SmartPtrToConst<T> {
public:
    SmartPtr(T *p):SmartPtrToConst<T>(p)
    {
        printf("this is SmartPtr ctro, constPointee is %p, pointee is %p\n", this->constPointee, this->pointee);
    }
    void fun2(T a)
    {
        *(this->pointee) = a;
    }
};

int main()
{
    int a = 3;

    SmartPtr<int> sp1 = &a;
    SmartPtrToConst<int> sp2 = sp1;
    sp2.fun1();

    sp1.fun2(4);
    printf("a is %d\n", a);
    sp2.fun1();
}

 结果如下:

this is SmartPtrToConst ctro, constPointee is 0x7fffd103831c, pointee is 0x7fffd103831c
this is SmartPtr ctro, constPointee is 0x7fffd103831c, pointee is 0x7fffd103831c
in smartptrotconst, read const ptr: 3
a is 4
in smartptrotconst, read const ptr: 4

 

posted @ 2018-09-02 20:40  gqtc  阅读(224)  评论(0编辑  收藏  举报