为什么派生模板类无法访问基础模板类的标识符?

44

考虑:

template <typename T>
class Base
{
    public:
        static const bool ZEROFILL = true;
        static const bool NO_ZEROFILL = false;
}

template <typename T>
class Derived : public Base<T>
{
    public: 
        Derived( bool initZero = NO_ZEROFILL );    // NO_ZEROFILL is not visible
        ~Derived();
}

我无法使用GCC g++ 3.4.4 (cygwin)进行编译。

在将它们转换为类模板之前,它们不是泛型的,派生类能够看到基类的静态成员。这种可见性的丧失是否符合C++规范要求,还是需要采用某种语法更改?

我知道每个Base<T>实例都有自己的静态成员"ZEROFILL"和"NO_ZEROFILL", Base<float>::ZEROFILLBase<double>::ZEROFILL是不同的变量,但我并不关心;这个常量存在是为了增加代码的可读性。我想使用一个静态常量,因为这比宏或全局变量更安全,可以避免名称冲突。


由于双阶段名称查找(并非所有编译器都默认使用),因此有四种解决方案:1)使用前缀Base<T>::NO_ZEROFILL2)使用前缀this->NO_ZEROFILL3)添加语句using Base<T>::NO_ZEROFILL4)使用启用宽容模式的全局编译器开关。这些解决方案的优缺点在https://dev59.com/Eek5XIcBkEYKwwoY7eQE中有描述。 - George Robinson
4个回答

49
那就是针对你的两阶段查找。`Base::NO_ZEROFILL`(所有大写标识符都是错误的,除了宏)是一个依赖于`T`的标识符。因为当编译器首次解析模板时,尚未替换`T`的实际类型,所以编译器不知道`Base`是什么。因此,您不能假定任何您认为在其中定义的标识符(可能有一些`T`的特化,编译器只能在稍后看到)。您不能省略基类限定符中定义的标识符。这就是为什么您必须编写`Base::NO_ZEROFILL`(或`this->NO_ZEROFILL`)的原因。这告诉编译器`NO_ZEROFILL`是依赖于`T`的基类中的内容,并且它只能在稍后通过实例化模板来验证。因此,编译器将接受它而不尝试验证代码。该代码只能在提供`T`的实际参数实例化模板时进行验证。

1
啊,你比我快了20秒。+1 - jalf
1
@jalf:这一次,我是第一个。:^> 随意改进它。 - sbi
2
有趣。那么继承的非静态成员是否需要相同的限定符呢?即 Base<T>::memberFunction()。 - cheshirekow
2
@ceshirekow:是的,基本上在::后面的所有东西,在::之前有一个依赖于模板参数的东西,都是“相关的”,这是由此引起的问题之一。(如果您想了解更多信息,请参见为什么有时需要使用typename的解释。它具有相同的原因,并且您将在各处找到很好的解释。) - sbi
1
@sbi:你确定这被称为二阶段查找吗?我一直认为这个术语只是针对参数相关的查找。你有标准中的参考资料吗? - Richard Corden
显示剩余7条评论

30
您遇到的问题是由于依赖基类名称查找规则造成的。14.6/8规定:
在查找模板定义中使用的名称声明时,非依赖名称使用通常的查找规则(3.4.1、3.4.2)。依赖于模板参数的名称的查找将被推迟,直到知道实际的模板参数(14.6.2)。
(这并不是真正的“两阶段查找”——请参见下面的说明。)
14.6/8的要点在于,就编译器而言,在您的示例中NO_ZEROFILL是一个标识符,不依赖于模板参数。因此,它会按照3.4.1和3.4.2中的普通规则进行查找。
这种普通查找不会搜索Base<T>内部,因此NO_ZEROFILL只是一个未声明的标识符。14.6.2/3规定:
如果类模板的基类依赖于模板参数,则在类模板或类模板成员的定义点或类模板或其成员的实例化期间,在未限定名称查找时不检查基类范围。
当您用Base<T>::限定NO_ZEROFILL时,实质上您将其从非依赖名称更改为依赖名称,这样做会推迟其查找,直到模板被实例化。
附注:什么是两阶段查找:
void bar (int);

template <typename T>
void foo (T const & t) {
  bar (t);
}


namespace NS
{
  struct A {};
  void bar (A const &);
}


int main ()
{
  NS::A a;
  foo (a);
}

上面的例子编译过程如下。编译器解析foo函数体并发现调用了一个依赖参数的函数bar。此时,编译器按照3.4.1进行查找,这是"第一阶段查找"。查找将找到函数void bar (int),并将其存储在依赖调用中,直到稍后实例化模板时。
当模板被实例化(由main调用引起)时,编译器会在参数的作用域中执行额外的查找,这是"第二阶段查找"。在这种情况下,将找到void NS::bar(A const &)
编译器有两个bar重载,它在它们之间进行选择,在上述情况下调用void NS::bar(A const &)

4
+1,解释得很好 :) 很遗憾你似乎已经停止在stackoverflow上回答问题了。我喜欢你详尽而深入的讨论。 - Johannes Schaub - litb
被想念是件很美好的事情!我希望能在不久的将来回到回答问题。 - Richard Corden
@RichardCorden 我没有完全理解你的旁注来说明“两阶段查找”。据我理解,在foo函数体中的函数名bar是一个相关名称,因为它的参数是类型相关的。难道在foo中对bar的名称查找也不应该推迟到实际模板参数已知之后吗?事实上,即使我注释掉了void bar(int);这一行,clang++ 3.8.1也不会产生任何关于名称查找的错误。所以我怀疑bar的“第一阶段查找”是否真的被执行了。 - Carousel
@Carousel 由于名称依赖性,不应在第一阶段查找期间出现错误。请尝试以下操作:在 foo(T const &) 上方添加一个新的模板重载,template <typename T> void bar (T);。更改 namespace NS 中的重载,使其具有相同的签名:template <typename T> void bar (T);。第一阶段查找会发现 bar 的两个重载版本,而第二阶段会发现 NS::bar。编译器无法在 bar(T) 函数之间选择最佳匹配项。现在将全局 bar(T) 移动到 foo 下面。代码将成功编译,因为第一阶段只会找到 bar(int) - Richard Corden

1

在 VS 2008 中似乎可以编译通过。你试过了吗:

public:
    Derived( bool initZero = Base<T>::NO_ZEROFILL );

5
实际上,VC不执行双相查找。这就是为什么它在那里编译的原因。这也是使用VC创建模板库的坏主意之所在--当您需要在任何其他编译器上使用它时,您将有很多东西需要修复。 - sbi
修复通常相当简单。主要是插入大量的 typename,并修复偶尔出现的两阶段查找问题。 - jalf
3
@jalf:那是真的,但有些情况除外。我遇到过非常严重的相互依赖问题,这些问题在使用VC时无法发现,因为VC只会在实例化模板时才真正解析模板 - 到那时所有相关实体都已在范围内。在第一次适当的两阶段查找中解析时,这个问题就变得一塌糊涂了,需要相当长的时间和程序员的通用解决方法(间接引用)来解决混乱。之后,代码变得更难理解,如果这个问题早些时候被发现,它可能会被设计得不同。 - sbi

0

试试这个程序

#include<iostream>
using namespace std;
template <class T> class base{
public:
T x;
base(T a){x=a;}
virtual T get(void){return x;}
};
template <class T>
class derived:public base<T>{
public:
derived(T a):base<T>(a){}
T get(void){return this->x+2;}
};
int main(void){
base<int> ob1(10);
cout<<ob1.get()<<endl;
derived<float> ob(10);
cout<<ob.get();
return 0;
}

T get(void){return this->x+2;}这行中,你也可以使用作用域解析(::)运算符。例如,尝试将该行替换为

T get(void){return base<T>::x+2;}

1
请正确格式化您的代码。解释代码为什么有效以及原始代码中存在问题的位置通常也是一个好习惯。 - stefan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接