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

时间:2019-05-22 10:07:29

标签: c++ templates inheritance language-lawyer using-declaration

对模板基类成员的访问需要语法this->memberusing指令。这种语法是否还会扩展到未直接继承的基本模板类?

考虑以下代码:

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;
}

由于模板类B的声明包含using A<X>::x,因此派生的模板类C自然可以使用x访问using B<X>::x。不过,在g ++ 8.2.1和clang ++ 6.0.1上,上述代码可以很好地编译,其中x可以通过Cusing中访问,x直接从{ {1}}

我希望A无法直接访问C。另外,在A中注释掉using A<X>::x仍使代码得以编译。甚至在B中注释掉using A<X>::x并同时在B C中使用而不是using B<X>::x中使用的组合也可以编译。

代码合法吗?

添加

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

4 个答案:

答案 0 :(得分:4)

您正在使用A<X>,因为它应该是基类。

  

[namespace.udecl]

     

3在用作成员声明的using声明中,每个   using-declarator的nested-name-specifier应命名为   正在定义的类。

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

  

[温度]

     

9查找模板中使用的名称声明时   定义,通常的查找规则([basic.lookup.unqual],   [basic.lookup.argdep])用于非相关名称。的查询   取决于模板参数的名称将推迟到   实际的模板参数是已知的([temp.dep])。

因此允许这样做是因为编译器无法更好地了解它。实例化该类时,它将检查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> {};

使以上声明正确无误。

答案 1 :(得分:2)

  

代码合法吗?

是的。这就是公共继承。

  

是否可以允许从B派生的模板类仅使用B :: x或B :: x通过this-> x访问x? ...

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

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


关于添加,请注意:

(1)编译器知道给定A<X>::x的某些实例,A<X>指的是什么对象(因为A是在全局范围内定义的,而X是模板参数C

(2)您确实有一个A<X>的实例-this是派生类的子对象(A<X>是否是直接基类都没有关系)。

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

using语句仅仅是语法糖。解决所有类型后,编译器将在实例中使用适当的内存地址替换x之后的使用,这与直接写入this->x一样。

答案 2 :(得分:1)

也许这个例子可以使您了解为什么它是合法的:

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; }的情况下,最后继承的xB::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的程序员知道这两个成员,那么允许使用此功能似乎非常合理!

关于在using A<X>::x;中使用编译器时为什么应该能够识别C<X>,请考虑以下事实:在类/类模板的定义中,所有直接/间接继承的基团都是可见的不管继承的类型。但是只有公开继承的文件才可以访问!

例如,例如:

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>上!

祝你好运!

答案 3 :(得分:-1)

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 ++的设计目标。在实例化时检查了许多成形性良好的约束,这很好。

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

必须对名称进行限定以使其依赖的原则 not 不能对模板代码中的错误进行早期诊断;设计人员认为在模板中使用名称查找的方式必须支持模板代码中的“合理”(实际上是少了一些疯狂)的名称查找:在模板中使用非本地名称不应经常将 绑定到客户端代码声明的名称,因为这会破坏封装和位置。

请注意,对于任何不合格的从属名称,如果它最适合重载分辨率,则可能会意外地调用一个不相关的冲突用户函数,这是真正的概念合同可以解决的另一个问题。

考虑此“系统”(即不属于当前项目的一部分)标题:

// 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)
}

这不是故意的,用户功能可能具有完全不同的行为,并在运行时失败。当然,它也可能具有不兼容的返回类型,并因此而失败(显然,如果库函数返回的值与该示例不同)。或者它可能在重载解析期间造成歧义(如果函数带有多个参数并且库和用户函数均为模板,则可能涉及更多情况)。

如果这还不够糟糕,现在考虑链接use_bar1.cc和use_bar2.cc;现在,我们在不同的上下文中使用了相同的模板函数,导致了不同的扩展(用宏说,因为模板仅比美化的宏稍好一点);与预处理器宏不同,您不允许这样做,因为两个翻译单元以两种不同的方式定义了相同的具体功能bar(UserType)这是一个ODR违规,该程序不适用于诊断程序。这意味着,如果实现在链接时没有捕获到错误(很少会这样做),则运行时的行为从一开始就是不确定的:程序的运行没有定义行为。

如果您有兴趣,可以在D&E(C ++的设计和演变)中讨论在ISO标准化之前的“ ARM”(带注释的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 

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

请注意,如果没有useful_lib命名空间,仍然可以找到另一个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之间唯一的声明性差异是标头的包含顺序:

  • user1导致实例化的bar<long>在没有foo(int)的情况下可见,并且foo的名称查找仅找到template <typename T> foo(T)签名,因此绑定显然完成该功能模板;

  • user2使由bar<long>定义的foo(int)实例化可见,因此名称查找同时找到foo和非模板名称是更好的匹配;重载的直观规则是,可以匹配较少参数列表的所有内容(函数模板或常规函数)都将获胜:foo(int)仅可以完全匹配int,而template <typename T> foo(T)可以匹配任何内容(复制)。

因此,两个TU的链接再次导致ODR违反;最可能的实际行为是可执行文件中包含的功能是不可预测的,但是优化的编译器可能会假设user1()中的调用未调用foo(int)并生成对{{1 }}恰好是调用bar<long>的第二个实例,这可能会导致生成不正确的代码[假设foo(int)仅可以通过foo(int)进行递归,而编译器则认为没有” t递归并对其进行编译,以使递归被破坏(如果该函数中有一个已修改的静态变量,并且编译器在函数调用之间移动修改以折叠连续的修改,则可能是这种情况)。

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

但是在您的情况下,没有这样的名称绑定问题,因为在这种情况下,using声明只能命名(直接或间接)基类。编译器在定义时无法知道是直接的还是间接的基础还是错误并不重要。它将在适当的时候检查。

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

s

诊断模板定义时实际上并不重要,并且对于使编译器符合要求也不是必需的(而且我不相信编译器作者会尝试这样做)。

仅在实例确定要诊断的问题时进行诊断是可以的;它并没有违反C ++的任何设计目标。