条款四十二:了解typename的双重意义

顾名思义,typename有双重含意。只要你用过template,那么第一重含意一定知道,那就是声明模板的时候,我们既可以这样写:

template <class T>

也可以这样写

template <typename T>

这两种写法并没有任何区别,都是标记T可以是符合隐式接口的任何类型,包括系统预定义类型,也包括用户自定义类型。
typename的第二重含意其实不大能遇到,因为这个依赖于编译器,看下面的例子:

class SampleClass
{
public:
    typedef int MyInt;
// static const int MyInt = 3;
};

int main()
{
    SampleClass::MyInt *b = new int(3); // 有的编译器会报错
}

MyInt来源于SampleClass内的一个重定义,MyInt等价于int,所以main函数里面实质上是int *b = new int (3),但有的编译器会报error,这是因为编译器在遇到这句代码上会产生歧义,因为它可以将MyInt作为SampleClass的一个静态对象来看(可以看程序被注释掉的代码的地方),这样就变成了一个静态对象乘以b了。这种“看法”有些不可思议,但在有些编译器上,却是优先将之视为静态变量的,而不是类型。为了解决这个二义性,在前面加上typename,这样就会强制让编译器将之视为一个类型,而不是静态变量了。像这种SampleClass::MyInt或是书上举的T::const_iterator,都是类中定义的名字,这种名字称之为dependent names(嵌套从属名称),所有dependent names都潜在具有二义性,为了消除二义性,就在前面加上typename,变成typename T::const_iterator,typename SampleClass:MyInt,这样就会解决一些看似正确却怎么也编不过的代码问题了。
使用typename有两个特例,一个是继承的时候,像下面这样:

1 class A: public B::NestedClass{}; // 正确
2 class A: public typename B::NextedClass(){}; // 错误

在继承XXX类时,即使这个类名是dependent names,也不要使用typename,因为编译器这时候显示不会将之翻译成静态成员(被继承类必定是个类型)。

另一个特例是构造函数时,对于构造成员列表,像下面这样:

1 A(): B::NestedClass(){} // 正确
2 A(): typename B::NestedClass(){} // 错误

这时候编译器也不会将之视为静态对象的,因为静态对象是不支持成员初始化列表这样的初始化形式的,所以这时的typename,反而会被编译器认为一个是BUG。

最后总结一下:

  1. 声明template参数时,前缀关键字class与typename可以互换
  2. 请使用关键字typename标识嵌套从属类型名称;但不得在base class lists或者member initialization list内使用typename

条款四十三:学习处理模板化基类的名称

背景是这样的,有两个不同的公司,然后想设计一个MessageSender,为这两个公司发送不同的消息,既支持明文发送SendClearText,也支持密文发送SendEncryptedText。一种思路是采用动态绑定的方法,定义一个BasicMessageSender,里面有两个方法,分别是发送明文和密文的虚函数,然后定义它的子类MessageSenderForCompanyA,以及MessageSenderForCompanyB,在这两个子类里面覆盖发送明文和密文的虚函数,从而达到根据不同公司发送不同消息的目的。

但这里我们想换一种思路,使用静态多态的方法来实现,静态多态就是模板的技术了,代码如下:

class CompanyA
{
public:
    void SendClearText(){}
    void SendEncryptedText(){}
};


class CompanyB
{
public:
    void SendClearText(){}
    void SendEncrypedText(){}
};

template <class T>
class MsgSender
{
public:
    void SendClearText(){}
};

template <class T>
class MsgSenderWithLog: public MsgSender<T>
{
public:
    void SendClearTextWithLog()
    {
        // Logs
        SendClearText(); // 有的编译器会编不过这段代码
    }
};

int main()
{
    MsgSenderWithLog<CompanyA> MsgSender;
    MsgSender.SendClearTextWithLog();
}

CompanyA与CompanyB有各自的发送函数,然后有一个模板类MsgSender,这个模板待确定的参数是T,可以是CompanyA或者CompanyB,这样就可以在定义MsgSender时,比如MsgSender或者MsgSender,指定到底调用的哪个公司的发送函数了,这是在编译期就可以确定下来的事情。

但现在有一个新问题,那就是我们希望在执行发送函数之前,还是加上日志比较好,这样我们就继承了MsgSender,定义了一个新类MsgSenderWithLog,在这里定义了一个新的函数SendClearTextWithLog,在这个函数里面调用了父类的SendClearText。

对于这段代码,其实思路还是挺清晰的,但问题是有的编译器会编不过这行代码(VS2008以后的版本的都是可以的,之前的版本没试),为什么?

这是因为在模板技术中存在全特化的概念,比如C公司,这个公司根本不想发送明文,也就是说它只有SendEncryptedText()接口,没有SendClearText()。为了使我们的静态多态仍然可用,我们这样定义只适用于C公司的MsgSender:

class CompanyC
{
public:
    void SendEncryptedText(){}
};

template <>
class MsgSender<CompanyC>
{
public:
    void SendEncryptedText(){}
};

这时候如果去调用:

1 MsgSenderWithLog MsgSenderC;
2 MsgSenderC.SendClearTextWithLog(); //编译器无法通过编译
这样编译器会报找到SendClearText()的错。正是因为有的编译器考虑到了全特化模板版本可以与普通版本不同,所以在有继承关系存在时,对直接调用父类的函数给出了不支持的error。但这个error是与编译器相关的,不是必然出现的。

为了让更多的编译器放弃这种全特化的忧虑,书上提供了三种解决方法:

方法一:

void SendClearTextWithLog()
{
    // Logs
    SendClearText(); // 有的编译器会编不过这段代码
}
改成
void SendClearTextWithLog()
{
    // Logs
    this->SendClearText(); // 这下能编译通过了
}

方法二:

在子类中声明using MsgSender<T>::SendClearText;
编译器报error本质是不进行模板父类域的查找,所以这里using了父类的一个函数名,强制编译器对之进行查找。

方法三:

将SendClearText()指明为MsgSender::SendClearText()。

最后总结一下:
可在derived class template内通过“this->”指涉base class templates内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成。