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

Visibility of members of base template class not directly inherited

本文关键字:成员 可见性 继承      更新时间:2023-10-16

访问模板基类的成员需要语法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可以通过using B<X>::x访问x。尽管如此,在 g++ 8.2.1 和 clang++ 6.0.1 上,上面的代码编译得很好,其中xC访问,using直接从A获取x

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

代码合法吗?

加法

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

您正在使用A<X>需要基类的地方。

[命名空间.udecl]

3 在用作成员声明的使用声明中,每个 使用声明符的嵌套名称说明符应命名基类 正在定义的类。

由于这出现在需要类类型的位置,因此已知并假定它是一种类型。它是一种依赖于模板参数的类型,因此不会立即查找。

[温度]

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

使上述声明正确无误。

代码合法吗?

是的。这就是公共继承的作用。

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

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

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


关于添加,请注意:

(1)编译器知道给定A<X>的某个实例A<X>::x引用的对象(因为A是在全局范围内定义的,XC的模板参数)。

(2)你确实有一个A<X>的实例 -this是派生类的ponter(A<X>是否是直接基类并不重要)。

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

use 语句只是语法糖。解析所有类型后,编译器会将以下使用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;,请考虑这样一个事实,即在类/类模板的定义中,无论继承类型如何,所有直接/间接继承的基础都是可见的。但只有公共继承的才能访问!

例如,如果它像:

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>

祝你好运!

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++可能太复杂和不规则,无法拥有真正的概念和真正的模板单独检查。

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

请注意,对于任何非限定的依赖名称,如果它与重载分辨率更匹配,则最终可能会意外调用不相关的冲突用户函数,这是真正的概念协定可以解决的另一个问题。

考虑这个"系统"(即不是当前项目的一部分)标头:

// 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 违规,程序格式不正确,无需诊断。这意味着,如果实现在链接时没有捕获错误(很少有人这样做),则运行时的行为从一开始就未定义:没有程序运行已定义行为。

如果你有兴趣,在"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 

在这里,名称绑定是这样完成的,没有更好的重载匹配(即非模板函数不匹配)可以"击败"专用化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会导致使用foo(int)可见定义的bar<long>的实例化,因此名称查找会同时找到foo,而非模板查找是更好的匹配;重载的直观规则是,任何可以匹配较少参数列表的东西(函数模板或常规函数)都胜出:foo(int)只能完全匹配一个int,而template <typename T> foo(T)可以匹配任何内容(可以复制)。

因此,两个 TU 的链接再次导致 ODR 违规;最可能的实际行为是可执行文件中包含的哪个函数是不可预测的,但优化编译器可能会假设user1()中的调用不会调用foo(int)并生成对bar<long>的非内联调用,这恰好是最终调用foo(int)的第二个实例, 这可能会导致生成不正确的代码 [假设foo(int)只能通过user1()递归,并且编译器看到它不会递归并对其进行编译,以便递归被破坏(如果该函数中有一个修改的静态变量并且编译器在函数调用之间移动修改以折叠连续修改,则可能出现这种情况)]。

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

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

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

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

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

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