[TOC]

问题的描述

今天尝试着用C++实现一个简单的doubly linked list的时候碰到一个问题, 先看下面的代码.

template <typename Object>
class DoublyLinkedList {
    // codes omitted
    public:
    class const_iterator {
        protected:
            Node* current;
            Object& retrieve() const {return current->data;}
        // codes omitted
    }
    class iterator : public const_iterator {
        // current and retrieve() inherited from const_iterator
        public:
            Object& operator* () {return retrieve();}
            iterator& operator++() {
                current = current->next;
                return *this;
            }
            iterator& operator--() {
                current = current->prev;
                return *this;
            }
    }
    // codes omitted
}

// in cpp file
#include "DoublyLinkedList.h"
#include <iostream>
using namespace std;

int main() {
    // call operator*(), operator++() or operator--()
    // of iterator class cause compiler errors
}

咋看之下, 上面的代码没有任何错误, 那么就看一看g++编译器对于这段代码怎么说吧?

>$ g++ DoublyLinkedList.cc
>In file included from DoublyLinkedList.cc:1:0:
>DoublyLinkedList.h: In member function 'Object& DoublyLinkedList<Object>::iterator::operator*()':
>DoublyLinkedList.h:59:47: error: there are no arguments to 'retrieve' that depend on a template parameter, so a declaration of 'retrieve' must be available [-fpermissive]
>DoublyLinkedList.h:59:47: note: (if you use '-fpermissive', G++ will accept your code, but allowing the use of an undeclared name is deprecated)
>DoublyLinkedList.h: In member function 'DoublyLinkedList<Object>::iterator& DoublyLinkedList<Object>::iterator::operator++()':
>DoublyLinkedList.h:64:13: error: 'current' was not declared in this scope
>DoublyLinkedList.h: In member function 'DoublyLinkedList<Object>::iterator& DoublyLinkedList<Object>::iterator::operator--()':
>DoublyLinkedList.h:73:13: error: 'current' was not declared in this scope

编译错误!!(这不是废话么, 不是编译错误哪里来的这篇文章!!)

最开始的想法

那么就让我就用我一开始的看法来分析一下吧.

  1. 首先const_iterator类中定义了两个protected符号, 一个是current变量, 另外一个是retrieve()函数, 那么protected是什么意思呢? C++标准(n3690)的第11节Member access control的开头写道: >protected; that is, its name can be used only by members and friends of the class in which it is declared, by class derived from that class, and by their friends.

就是说, protected标识下的符号名称可以在派生类中使用咯?

  1. 根据第一点所述, 符号名称currentretrieve()iterator类当中应该都是存在的(或者说可见的, 可以被编译器看见的). 那么上面的代码应该是能够编译通过的.

上面就是我一开始的想法了. 实际上这段代码在g++编译器下面, 上面也说了, 是通过不了编译器这一关的,而在visual studio下面用msvc的编译器是能够通过的, 然而, 这个应该是msvc的编译器搞错掉的一个地方, 这种写法是不应该被编译器放过去的.

那么下面, 就来看这种写法为什么是一种错误, 为什么msvc的编译器在这上面(应该)犯了一个错误.

问题的解析

错误观察

既然是编译过程出现错误, 那么回过头还是应该从编译器给出的错误信息出发. 自习观察, 编译器的错误信息的第3说道:

there are no arguments to 'retrieve' that depend on a template parameter, so a declaration of 'retrieve' must be available.

然后在第6和第8行都提到了:

'current' was not declared in this scope.

上面的错误信息, 就相当于编译器对我们说:"不好意思, 我没有看见retrievecurrent这两个符号."

其实也就是说, 我们的这一段代码, 在iterator类中没有定义retrievecurrent这两个符号.

没有定义符号? 在const_iterator类中明明定义了protected属性的这两个符号啊? 然后iterator继承了这两个符号(应该是吧? 问问自己)

没有定义符号? 奇怪了?

-(@-@)-

错误定位与铲除

再仔细看一遍g++给出的错误信息, 有一组词显得很显眼:

...there are no arguments to 'retrieve' that depend on a template parameter, so...

这里的template parameter显得很扎眼. 然后回想, 在DoublyLinkedList类的开头, 定义了这个类是泛型类. 而iteratorconst_iterator这两个类都嵌套在DoublyLinkedList类当中, 而且都用到了typename Object中的Object. 自然, 这两个类肯定自身也是泛型类了. 那么错误会不会和这两个类是泛型类有关呢? 以我的水平, 去找C++标准然后慢慢翻看关于Template的规范挑错误肯定是一件超级艰巨的任务, 那么就先利用网上的资源吧, 全世界那么多程序员, 要相信, 肯定会有人碰到和你一样的问题的.

果然, 在万能的StackOveflow上面就找到了一个类似的问题"g++ template parameter error", 随然问题代码是不一样的, 但是最终的回答对于问题的解释一样的.

在StackOverflow的问题答案中, 排名第一的答案如此回答这个问题:

...the problem is that when you write a template class that inherits from a template base class, the way that name lookup works is slightly different from name lookup in normal classes. In particular, you cannot just reference base class members using their names; you need to indicate to the compiler where to look for the name. The reason this works in Visual Studio is that the Microsoft C++ compiler actually gets this behavior wrong and allows code that is technically illegal to compile just fine. ...

这个答案说的很清楚, 在模板类的name lookup和普通类的name lookup是不一样的. 在模板派生类中, 如果要使用模板基类中所定义的符号名称, 你需要直接告诉编译器说"我需要哪个类的哪个符号". 不然的话, 编译器是不知道去哪里查找这些符号的.

那么正确的做法应该是怎么样的呢? 很简单, 就是在retrievecurrent前面加上this->或者const_iterator::直接说明就好了.

对于错误的思考

但是, 知其然, 好像还没有知其所以然? 话说编译器为什么找不到这些个符号呢?

于是我又开始了翻书的旅途, 不过过程还算相对简单, 在C++神书Effective C++的第43个项目中, 就找到了相应的回答.

在Effective C++书上有例子用于说明这个问题, 相比来说, 还是书上的例子更加直观, 那么就用书上的例子来阐述这个问题好了(魂淡, 不就是抄书么, 冠冕堂皇=.=).

假设我们有一段用于给不同公司发送信息的代码. 信息可以用明文发送, 也可以经过加密之后发送. 我们假定在编译时期我们就有足够的信息来决定将哪一个信息发送到哪一家公司, 这样我们可以写出如下的代码:

class CompanyA {
public:
    ...
    void sendClearText(const std::string &msg);
    void sendEncryptedText(const std::string &msg);
    ...
};

class CompanyB {
public:
    ...
    void sendClearText(const std::string &msg);
    void sendEncryptedText(const std::string &msg);
    ...
};

... // 其他公司的classes

class MsgInfo {...} // 用于保存信息的类

template<typename Company>
class MsgSender {
public:
    ...  
    void sendClear(const MsgInfo &info) {
        std::string msg;
        ... // 根据info产生信息
        Company c;
        c.sendClearText(msg);
    }
    vodi sendSecret(const MsgInfo &info) {...} // 类似sendClear, 调用c.sendEncrypted()
}

上面的代码是行得通的, 不会产生错误. 那么, 假设这时候, 我们想要在每次送出信息的时候, 将一些信息记录(log)下来, 我们应该怎么办呢? 很自然的想法是: 实现一个继承自MsgSender的派生类, 在这个派生类中做log的事情:

template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
    public:
    ...
    void sendClearMsg(const MsgInfo &info) {
        ... // log before sending
        sendClear(info); // 调用base class函数, 编译错误
        ... // log after sending
    }
}

啊哈, 碰到和我的问题一样的问题, 编译通不过, 编译器找不到这个sendClear符号. 下面是Scott Meyers的解释:

The problem is that when compilers encounter the definition for the class template LoggingMsgSender, they don't know what class it inherits from. Sure, it's MsgSender<Company>, but Company is a template parameter, one that won't be known until later (when LogginMsgSender is instantiated). Without knowing what Company is, there's no way to know what the class MsgSender<Company> looks like. In particular, there's no way to know if it has a sendClear function.

Meyers的解释说, 因为Company是一个模板参数, 编译器在压根不知道MsgSender<Company>里面是否包含了一个sendClear函数.

等等, 编译器不知道是否存在sendClear函数么? 在MsgSender当中不是明确写出来了么? 编译器你真傻, 这都看不到?

那么, 真的是这样的么? 当然不是, 是的话, 还写个鸟啊!?

再看下面的例子, 假设我们有一个class CompanyZ, CompanyZ表示老子就是拽, 老子就是要用加密传输, 那么, 就会出现下面的代码:

class CompanyZ {
public:
    ...
    void sendEncrypted(const std::string &msg);
    ...

可以看到, CompanyZ是不符合一般性的MsgSender模板的, 因为这个模板提供了一个sendClear函数, 而CompanyZ类中并没有定义这个函数. 那么为了矫正这个问题, 我们可以针对CompanyZ做一个MsgSender的特化版:

template<>                                          // 一个全特化的MsgSender
class MsgSender<CompanyZ> {
public:
    ...
    void sendSecret(const MsgInfo &info);
    ...
}

关于特化版, 参考这里.

然后, 让我们再次考虑派生类LoggingMsgSender:

template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
    ...
    void sendClearMsg(const MsgInfo &info) {
        ... // log before send
        sendClear(info);  // 如果Company == CompanyZ, 这个函数不存在
        ... // log after send
    }
}

可以看到, 当基类被指定为MsgSender<CompanyZ>的时候, 这段代码不合法, 因为CompanyZ中并没有提供sendClear()函数.

这就是为什么C++拒绝这个调用的原因:编译器知道基类模板可能被特化, 而那个特化版本可能不提供和一般性template相同的接口. 因此它往往拒绝在模板化基类(MsgSender<Company>, const_iterator<Object>)内寻找继承而来的名称.

解决的方法上面已经提过了, 明确地告诉编译器, 我要调用那个基类模板中的函数.

具体方法有三, 简单说一下, 具体可以参考Effective C++第43章:

  1. 在调用前面加上this->, 这一点我自己也有点迷茫, 为什么加上this->就能行了
  2. 使用using声明式. using MsgSender<Company>::sendClear;
  3. 明确地调用位于基类模板中的函数. MsgSender<Company>::sendClear(info);. 这个方法是最令人拙计的方法, 因为如果这个函数是一个virtual函数, 这个明确的行为会将"virtual绑定行为"屏蔽掉.

这样解释, 就知道为什么说MSVC的编译器在这一点(应该是)犯了错误.

结语

一大堆解释, 算是搞懂了一点C++关于模板编程的问题, 但是比较不爽的, 最后还没有搞懂为什么加上this->就有效了, 而且水平比较有限, 没有能够翻看C++的标准去看到标准里面对于这个的解释, 比较遗憾.

但是, 不管如何, 还是算是学到了一点东西. ^-^

完整的doubly linked list的代码, 戳这里