Effective C++读书笔记(27)

来源:互联网 发布:徐静蕾字体 mac 编辑:程序博客网 时间:2024/06/06 12:24

条款41:了解隐式接口和编译期多态

Understand implicit interfaces andcompile-time polymorphism

面向对象编程以显式接口和运行期多态为中心。例如:

class Widget {
public:
    Widget();
    virtual ~Widget();
    virtual std::size_t size() const;
    virtual void normalize();
    void swap(Widget& other);
    ...
};

voiddoProcessing(Widget& w)
{
    if (w.size() > 10 && w !=someNastyWidget) {
        Widget temp(w);
        temp.normalize();
        temp.swap(w);
    }
}

我们可以这样说doProcessing 中的 w:

·    因为w被声明为Widget类型,w必须支持Widget接口。我们可以在源代码中找到这个接口(例如Widget的.h文件)以看清楚它是什么样子的,所以我们称其为一个显式接口——它在源代码中显式可见。

·    因为Widget的一些成员函数是virtual,w对这些函数的调用就表现为运行期多态:被调用的特定函数在执行期基于w的动态类型来确定。

 

模板和泛型编程以隐式接口和编译期多态为中心,显式接口和运行期多态的重要性降低。当我们把 doProcessing 从函数转为函数模板时:

template<typename T>
void doProcessing(T& w)
{
    if (w.size() > 10 && w !=someNastyWidget) {
        T temp(w);
        temp.normalize();
        temp.swap(w);
    }
}

现在我们可以这样说doProcessing中的w:

·    w 必须支持的接口是通过模板中在w身上所执行的操作确定的。在本例中的size,normalize 和 swap成员函数;copy构造函数(用于创建 temp)以及对不等于的比较(用于和someNastyWidget之间的比较),这一组表达式(对此template而言必须有效编译)便是T必须支持的一组隐式接口。

·    对诸如 operator> 和 operator!= 这样的包含 w 的函数的调用可能伴随实例化模板,以使这些调用成功。这样的实例化发生在编译期间,因为用不同的模板参数实例化函数模板导致不同的函数被调用,因此以编译期多态著称。

编译期多态:哪一个重载函数该被调用?

运行期多态:哪一个virtual函数该被绑定?

 

下面解释显式接口和隐式接口的区别:

显式接口由函数签名式组成:函数名,参数类型,返回值,等等。例如,Widget类由一个构造函数,一个析构函数,以及函数 size,normalize 和 swap 组成,再加上参数类型,返回类型和这些函数的常量性(它也包括编译器生成的copy构造函数和copy赋值运算符)。

隐式接口有很大不同,它不是基于函数签名式的,而是由有效表达式组成的。再看一下doProcessing template 开始处的条件:

 If(w.size()> 10 && w != someNastyWidget

T必须支持size成员函数,但这个函数可以是从基类继承来的。这个成员函数不需要返回一个整数类型,甚至不需要返回一个数值类型。就本例而言,它甚至不需要返回一个定义有 operator> 的类型。它要做的全部就是返回类型X的一个对象,而X对象加上一个int(10的类型)必须能够调用operator>。这个operator>不需要取得一个类型 X 的参数,因为它可以取得一个类型 Y的参数,只要X和Y之间有一个隐式转换即可。

类似地,T 也不一定支持 operator!=,因为operator!= 可以接受一个类型 X 的对象和一个类型 Y 的对象,且T能转型为 X, someNastyWidget能转型为 Y,对 operator!= 的调用就是合法的(甚至operator&& 也能被重载,将上面的表达式的含义从一个连接词“与”转换到某些完全不同的意义)。

隐式接口仅仅是由一组有效表达式构成。这些表达式自身看起来可能很复杂,但是它们施加的约束通常是简单易懂的。如上例,整个表达式的约束是非常简单的:if 语句的条件部分必须是一个布尔表达式,所以"w.size() > 10 && w != someNastyWidget"必须与 bool 兼容。这就是模板doProcessing 施加于其类型参数T 之上的隐式接口的一部分。doProcessing 要求的其他隐式接口:copy构造函数,normalize 和 swap 的调用对于类型 T 的对象必须是合法的。

隐式接口对模板参数施加的影响正像显式接口对类对象施加的影响一样,这两者都在编译期被检查。与类提供的显式接口矛盾的方法使用对象,和在模板中使用不支持模板所要求的隐式接口对象一样,代码将无法编译。

·    类和模板都支持接口和多态。

·    对类而言,接口是显式的,并以函数签名为中心。多态性则通过 virtual函数发生于运行期。

·    对于模板参数,接口是隐式的,并基于有效表达式。多态性则通过模板实例化和函数重载解析发生于编译期。

 

条款42:了解typename的双重意义

Understand the two meanings of typename

在下面的模板声明中class和typename没有不同。

template<class T> class Widget;

template<typename T> class Widget;

在声明模板类型参数的时候,class和typename意义完全相同。一些程序员更喜欢用class,因为可以少输几个字母,其他人更喜欢typename,因为它暗示着这个参数不一定非要一个类类型。从C++的观点看,class和typename在声明一个模板参数时意义完全相同。

然而,C++并不总是把class和typename视为等同,有时你必须使用typename。为了理解这一点,我们先谈谈你会在模板中指涉的两种名称。

假设我们有个模板函数,接受一个STL兼容容器为参数,打印其第二元素值。下面是一种愚蠢的且不能通过编译的方法:

template<typename C> // 打印容器内的第二元素,不能通过编译
void print2nd(const C& container)
{
    if (container.size() >= 2) {
        C::const_iterator iter(container.begin()); // 取得第一元素的迭代器
        ++iter; // 移向第二元素
        int value = *iter;
        std::cout << value; //print the int
    }
}

iter的类型是 C::const_iterator,依赖于模板参数C 的类型。模板中的依赖于一个模板参数的名称,被称为从属名称。当从属名称嵌套在一个类的内部时,称之为嵌套从属名称。C::const_iterator 就是这样一个名称。Value为int。int是一个不依赖于任何模板参数的名称,这样的名称是非从属名称

嵌套从属名称会导致解析困难。例如:
C::const_iterator * x;

这看上去好像是我们将 x 声明为一个指向 C::const_iterator 的局部变量指针。但那仅仅是因为我们知道C::const_iterator是个类型,如果C::const_iterator 不是一个类型,如果C有一个静态成员变量碰巧就叫做const_iterator或x碰巧是一个全局变量的名称,则上面的代码就不是声明一个局部变量,而成为C::const_iterator乘以 x!当然,这听起来有些愚蠢,但它是可能的,而编写C++解析器的人必须考虑所有可能的输入。

当模板print2nd被解析时,尚未确知C是什么东西。C++有一条规则解决这个歧义:如果解析器在模板中遇到一个嵌套从属名称,它假定那个名称不是一个类型,除非你用其它方式告诉它:
typename C::const_iterator iter(container.begin());

通用的规则很简单:任何时候你想要在模板中指涉一个嵌套从属模板类型名称,你必须把typename 放在紧挨着它的前面。

 

typename 应该仅仅被用于标识嵌套从属类型名称,其它名字不应该用它。例如,下面这个接受一个容器和一个指向该容器的迭代器的函数模板:

template<typenameC>                  // typenameallowed (as is "class")
void f(const C& container,           // typename not allowed
        typename C::iterator iter); // typename required

C 不是一个嵌套从属类型名称(它不是嵌套于任何取决于模板参数的东西内的),所以在声明 container 时它不必被 typename 前置,但是 C::iterator 是一个嵌套从属类型名称,所以它必需被 typename 前置。

 “typename 必须前置于嵌套从属类型名称”规则的例外是,typename不可以出现在一个基类列表中的嵌套从属类型名称之前,或者在成员初值列中作为一个基类标识符。例如:

template<typenameT>
class Derived: public Base<T>::Nested { // 基类列表: 不允许typename
public:
    explicit Derived(int x) : Base<T>::Nested(x) // 成员初值列:不允许typename
    {
        typename Base<T>::Nested temp; // 其它时候:需要typename
        ...
    }
    ...
};

 

让我们来看最后一个 typename 的例子,假设我们在写一个接受一个迭代器的函数模板),而且我们打算为该迭代器指涉的对象做一份local复件temp:

template<typenameIterT>
void workWithIterator(IterT iter)
{
    typenamestd::iterator_traits<IterT>::value_type temp(*iter);
    ...
}

std::iterator_traits<IterT>::value_type仅仅是一个标准特性类的使用,相当于“类型为IterT的对象所指物的类型”。这个语句声明了一个局部变量temp,并将temp初始化为iter所指物。如果IterT是vector<int>::iterator,temp就是int;如果IterT是list<string>::iterator,temp就是string。因为 std::iterator_traits<IterT>::value_type是个嵌套从属类型名称(value_type 嵌套在 iterator_traits<IterT> 内部,而且 IterT 是一个模板参数),我们必须让它被 typename前置。

std::iterator_traits<IterT>::value_type太长,需要创建一个typedef:
typedef typenamestd::iterator_traits<IterT>::value_type value_type;

value_typetemp(*iter);

把"typedef typename"并列,是因为涉及嵌套从属类型名称。作为结束语,应该提及编译器与编译器之间对围绕typename的规则的执行情况不同。这就意味着typename和嵌套从属类型名称的交互作用会导致一些轻微的可移植性问题。

·    在声明模板参数时,class 和 typename 是可互换的。

·    用 typename 去标识嵌套从属类型名称;但不得在基类列表中或在成员初值列中作为基类标识符。

原创粉丝点击