基本模板 class 成员的可见性未直接继承

Visibility of members of base template class not directly inherited

访问模板库 class 的成员需要语法 this->memberusing 指令。此语法是否也扩展到不直接继承的基本模板 classes?

考虑以下代码:

template <bool X>
struct A {
  int x;
};

template <bool X>
struct B : public A<X> {
  using A<X>::x; // OK even if this is commented out
};

template <bool X>
struct C : public B<X> {
  // using B<X>::x; // OK
  using A<X>::x; // Why OK?
  C() { x = 1; }
};

int main()
{
  C<true> a;

  return 0;
}

既然模板classB的声明包含了using A<X>::x,自然派生模板classC可以访问到xusing B<X>::x。尽管如此,在 g++ 8.2.1 和 clang++ 6.0.1 上,上面的代码编译得很好,其中 xC 中访问,using 直接从 A

我本来以为 C 不能直接访问 A。另外,注释掉 B 中的 using A<X>::x 仍然可以编译代码。即使在 B 中注释掉 using A<X>::x 并同时在 C 中使用 using B<X>::x 而不是 using A<X>::x 的组合也会给出编译代码。

代码合法吗?

加法

更清楚一点:问题出现在 template classes 上,它与模板 classes 继承的成员的可见性有关。 通过标准 public 继承,A 的 public 成员可被 C 访问,因此使用 C 中的语法 this->x 确实可以得到访问 A<X>::x。但是 using 指令呢?如果 A<X> 不是 C 的直接基础,编译器如何正确解析 using A<X>::x

Is the code legal?

是的。这就是 public 继承的作用。

Is it possible to allow a template class derived from B to access to x only via this->x, using B::x or B::x? ...

您可以使用私有继承(即 struct B : private A<X>),并仅通过 B 的 public/protected 接口安排对 A<X>::x 的访问。

此外,如果您担心有隐藏成员,您应该使用 class 而不是 struct 并明确指定所需的可见性。


关于添加,注意:

(1) 编译器知道对象 A<X>::x 指的是给定的 A<X> 的一些实例(因为 A 是在全局范围内定义的,而 XC).

的模板参数

(2) 你确实有一个 A<X> 的实例 - this 是派生的 class 的一个 ponter(如果 A<X> 是直接基础 class 或不)。

(3)对象A<X>::x在当前作用域可见(因为继承和对象本身是public)。

using 语句只是语法糖。解析完所有类型后,编译器会将以下 x 的使用替换为实例中适当的内存地址,这与直接编写 this->x 不同。

也许这个例子可以让您了解为什么它应该合法:

template <bool X>
struct A {
  int x;
};

template <bool X>
struct B : public A<X> {
  int x;
};

template <bool X>
struct C : public B<X> {
  //it won't work without this
  using A<X>::x; 
  //or
  //using B<X>::x;
  C() {  x = 1; }
  // or
  //C() { this -> template x = 1; }
  //C() { this -> x = 1; }
};

如果选择 C() { this -> template x = 1; },最后继承的 x (B::x) 将分配给 1 而不是 A::x

可以简单地通过以下方式进行测试:

    C<false> a;
    std::cout << a.x    <<std::endl;
    std::cout << a.A::x <<std::endl;
    std::cout << a.B::x <<std::endl;

假设 struct B 的程序员不知道 struct A 成员,但是 struct c 的程序员知道两者的成员,这个特性似乎很合理被允许!

至于为什么编译器在 C<X> 中使用时能够识别 using A<X>::x; ,请考虑这样一个事实,即在 class/class 模板的定义中所有 direct/indirect 无论继承类型如何,继承的基数都是可见的。但只有公共继承的才可以访问!

例如:

using A<true>::x;
//or
//using B<true>::x;

那么这样做就会有问题:

C<false> a;

或者相反。因为 A<true>B<true> 都不是 C<false> 的基础,因此可见。但是因为它就像:

using A<X>::x;

因为使用通用术语 X 来定义术语 A<X>,所以它首先是可推导的,其次是可识别的,因为任何 C<X>(如果以后没有专门化)是间接基于 A<X> !

祝你好运!

您正在使用 A<X>,而预期的碱基 class。

[namespace.udecl]

3 In a using-declaration used as a member-declaration, each using-declarator's nested-name-specifier shall name a base class of the class being defined.

由于它出现在预期 class 类型的位置,因此它是已知的并被假定为一种类型。而且它是一种依赖于模板参数的类型,所以不会立即查找它。

[temp.res]

9 When looking for the declaration of a name used in a template definition, the usual lookup rules ([basic.lookup.unqual], [basic.lookup.argdep]) are used for non-dependent names. The lookup of names dependent on the template parameters is postponed until the actual template argument is known ([temp.dep]).

所以它是允许的,因为编译器无法更好地了解。当实例化 class 时,它将检查 using 声明。实际上,可以将任何依赖类型放在那里:

template<bool> struct D{};

template <bool X>
struct C : public B<X> {
  using D<X>::x; 
  C() { x = 1; }
}; 

X 的值已知之前,不会对此进行检查。因为 B<X> 专业化可以带来各种惊喜。例如可以这样做:

template<>
struct D<true> { char x; };

template<>
struct B<true> : D<true> {};

使上述声明正确

template <bool X>
struct C : public B<X> {
  // using B<X>::x; // OK
  using A<X>::x; // Why OK?
  C() { x = 1; }
};

问题是为什么不支持?因为 A<X>C 的主模板定义的特化基础的约束是一个只能回答的问题,并且只对特定的模板参数 X 有意义?

能够在定义时检查模板从来都不是 C++ 的设计目标。许多 formed-ness 约束在实例化时被检查,这很好。

[如果没有真正的概念(必要且足够的模板参数契约)支持,C++ 的任何变体都不会做得更好,而且 C++ 可能太复杂和不规则,以至于永远无法拥有真正的概念和真正的模板单独检查。]

必须限定名称以使其依赖的原则对模板代码中的错误进行早期诊断;名称查找在模板 中的工作方式被设计者认为是必要的,以支持 "sane"(实际上稍微不那么疯狂)在模板代码中查找名称 :在中使用非本地名称模板不应 过于频繁地 绑定到客户端代码声明的名称,因为它会破坏封装和局部性。

请注意,对于任何不合格的从属名称 如果它更适合重载解析,您可能会意外调用不相关的冲突用户函数,这是另一个将要解决的问题通过真正的概念合同。

考虑这个 "system"(即不是当前项目的一部分)header:

// useful_lib.hh _________________
#include <basic_tool.hh>

namespace useful_lib {
  template <typename T>
  void foo(T x) { ... }

  template <typename T>
  void bar(T x) { 
    ...foo(x)... // intends to call useful_lib::foo(T)
                 // or basic_tool::foo(T) for specific T
  }
} // useful_lib

那个项目代码:

// user_type.hh _________________
struct UserType {};

// use_bar1.cc _________________
#include <useful_lib.hh>
#include "user_type.hh"

void foo(UserType); // unrelated with basic_tool::foo

void use_bar1() {
  bar(UserType()); 
}

// use_bar2.cc _________________
#include <useful_lib.hh>
#include "user_type.hh"

void use_bar2() {
  bar(UserType()); // ends up calling basic_tool::foo(UserType)
}

void foo(UserType) {}

我觉得这个代码很现实合理;看看你是否能看到非常严重的非局部问题(只有通过阅读两个或更多不同的函数才能发现的问题)。

此问题是由于在库模板代码中使用了未记录名称的非限定依赖名称引起的(直觉不应该 是)或已记录但用户不感兴趣,因为他从不需要覆盖库行为的那部分。

void use_bar1() {
  bar(UserType()); // ends up calling ::foo(UserType)
}

这不是预期的,用户函数可能有完全不同的行为并在 运行 时失败。当然,它也可能具有不兼容的 return 类型并因此失败(如果库函数 returned 的值与该示例中的值不同,显然)。或者它可能会在重载解析期间产生歧义(如果函数采用多个参数并且库函数和用户函数都是模板,则可能会涉及更多情况)。

如果这还不够糟糕,现在考虑 linking use_bar1.cc 和 use_bar2.cc;现在我们在不同的上下文中两次使用相同的模板函数,导致不同的扩展(在 macro-speak 中,因为模板只比美化的宏稍微好一点);与预处理器宏不同,您不能这样做,因为两个翻译单元以两种不同的方式定义了相同的具体函数 bar(UserType)这是一个 ODR 违规,程序格式不正确 否需要诊断。这意味着如果实现没有在 link 时间捕获错误(很少有人这样做),那么在 运行 时间的行为从一开始就是未定义的:程序没有 运行定义的行为。

如果您有兴趣,在 "ARM"(带注释的 C++ 参考手册)时代,早在 ISO 标准化之前,模板中名称查找的设计在 D&E(C++ 的设计和演化)中进行了讨论).

至少对于限定名称和非依赖名称,可以避免这种无意的名称绑定。您无法使用非依赖的非限定名称重现该问题:

namespace useful_lib {
  template <typename T>
  void foo(T x) { ... }

  template <typename T>
  void bar(T x) { 
    ...foo(1)... // intends to call useful_lib::foo<int>(int)
  }
} // useful_lib 

这里完成名称绑定,因此没有更好的重载匹配(即非模板函数不匹配)可以 "beat" 专业化 useful_lib::foo<int> 因为名称绑定在上下文中模板函数定义,还因为 useful_lib::foo 隐藏了任何外部名称。

请注意,如果没有 useful_lib 命名空间,另一个 foo 恰好在之前包含的另一个 header 中声明的另一个 foo 仍然可以找到:

// some_lib.hh _________________
template <typename T>
void foo(T x) { }

template <typename T>
void bar(T x) { 
  ...foo(1)... // intends to call ::foo<int>(int)
}

// some_other_lib.hh _________________
void foo(int);

// user1.cc _________________
#include <some_lib.hh>
#include <some_other_lib.hh>

void user1() {
  bar(1L);
}

// user2.cc _________________
#include <some_other_lib.hh>
#include <some_lib.hh>

void user2() {
  bar(2L);
}

您可以看到 TU 之间唯一的声明性差异是 headers 的包含顺序:

  • user1 导致 bar<long> 的实例化没有 foo(int) 可见,foo 的名称查找仅找到 template <typename T> foo(T) 签名所以绑定显然是对该函数模板完成的;

  • user2 导致用 foo(int) 定义的 bar<long> 的实例化可见,因此名称查找同时找到 foo 和非模板一个更好匹配;重载的直观规则是任何可以匹配较少参数列表的东西(函数模板或常规函数)获胜:foo(int) 只能完全匹配 inttemplate <typename T> foo(T) 可以匹配任何东西(可以被复制)。

所以又是link同时使用两个 TU 会导致 ODR 违规;最可能的实际行为是可执行文件中包含哪个函数是不可预测的,但优化编译器可能会假设 user1() 中的调用不会调用 foo(int) 并生成对 [=26 的非内联调用=] 恰好是最终调用 foo(int) 的第二个实例化,这可能会导致生成不正确的代码 [假设 foo(int) 只能通过 user1() 递归并且编译器认为它没有' t 递归并编译它以破坏递归(如果该函数中有一个修改后的静态变量并且编译器将修改移动到函数调用之间以折叠连续修改,则可能是这种情况)。

这表明模板非常脆弱和脆弱,应格外小心使用。

但在您的情况下,不存在此类名称绑定问题,因为在该上下文中,using 声明只能命名(直接或间接)基 class。编译器在定义时无法知道它是直接基数还是间接基数或错误并不重要;它将在适当的时候检查。

虽然允许对固有错误代码进行早期诊断(因为 sizeof(T())sizeof(T) 完全相同,但 s 的声明类型在任何实例化中都是非法的:

template <typename T>
void foo() { // template definition is ill formed
  int s[sizeof(T) - sizeof(T())]; // ill formed
}

在模板定义时进行诊断实际上并不重要,也不需要符合标准的编译器(而且我不相信编译器作者会尝试这样做)。

仅在保证在该点捕获的问题的实例化点进行诊断是可以的;它不会破坏 C++ 的任何设计目标。