C++编译器在封装行为上存在分歧 - 哪一个做对了?

C++ compilers diverge in encapsulation behavior - which one gets it right?

本文关键字:哪一个 存在 编译器 封装 C++      更新时间:2023-10-16

编译器(clang-5.0.0GCC-7.3ICC-18MSVC-19)在下面A成员的可访问性方面有所不同。

class A {
template <class> static constexpr int f() { return 0; }
template <int> struct B {};
template <class T> using C = B<f<T>()>;
};

实际上,请考虑以下用法:

template <class T> using D = A::C<T>;
int main() {
//    | clang | gcc | icc | msvc
(void) A::f<int>(); // 1: | f     | f   | f   | f, (C)
(void) A::B<0>{};   // 2: | B     |     | B   | B, (C)
(void) A::C<int>{}; // 3: | C,  f |     | C   | C
(void) D<int>{};    // 4: | f     |     | C   | C
}

右表显示了每个编译器需要公开哪些成员才能接受代码(为 C++14 编译时)。

恕我直言,ICC 和 MSVC(忽略(C)条目)看起来是正确的。除了第一行,GCC似乎完全忽略了可访问性。

我不同意 clang 要求f公开以实例化A::C<int>D<int>。像ICC和MSVC一样,我认为C,只有C需要公开。确实C使用f但它不是实现细节吗?请注意,C也使用B。如果叮当是正确的,那么为什么它不要求B也是公开的呢?

最后,让我们考虑(C)条目。MSVC要求C在第一次遇到D的定义时是公开的,也就是说,MSVC抱怨C是私有的。

我的问题是:

  1. 我的分析是对的(ICC也是)吗?否则,还有哪个编译器是正确的,为什么?
  2. MSVC问题是否又是 MSVC 中的两阶段实例化错误?

更新:关于 GCC,这似乎是评论 8 中报告的错误。

A::f<int>()A::B<0>的问题很容易回答。fB是私有的,也没有任何其他有趣的依赖项。访问它们的格式应该不正确。gcc 通常对模板中的访问控制非常宽容,在各种情况下都有一个未完成的元错误(我认为所有这些都是 gcc 允许在不应该访问时访问的形式,而不是在应该允许访问时禁止访问)。

A::C<int>的问题更有趣。这是一个别名模板,但我们实际上在什么上下文中查看别名?它是A范围内(在这种情况下,使C可访问就足够了)还是在使用它的上下文中(在这种情况下,fBC都需要可访问)。这个问题正是CWG 1554,它仍然有效:

从 17.6.7 [temp.alias] 的当前措辞中不清楚别名模板和访问控制的交互。例如:

template <class T> using foo = typename T::foo;
class B {
typedef int foo;
friend struct C;
};
struct C {
foo<B> f;    // Well-formed?
};

B::foo替换为foo<B>是在友C类的上下文中完成的,使引用格式良好,还是独立于别名模板专用化的上下文确定访问权限?

如果这个问题的答案是访问是独立于上下文确定的,则必须注意确保访问失败仍被视为"在函数类型的直接上下文中"(17.9.2 [temp.deduct] 第 8 段),以便导致扣除失败而不是硬错误。

尽管问题仍然悬而未决,但方向似乎是:

CWG的共识是,别名模板的实例化(查找和访问)应与其他模板一样,在定义上下文中,而不是在使用它们的上下文中。但是,它们仍应立即扩大。

也就是说,只有C需要公开,fB可以保持私密。这就是ICC和MSVC的解释。Clang有一个错误,允许别名模板规避访问(15914),这就是为什么clang要求f可访问但不能B。但除此之外,clang 似乎在使用点而不是定义点扩展别名。

D<int>的问题应该完全遵循A::C,CWG 1554 在这里没有问题。Clang是唯一一个在A::CD之间具有不同行为的编译器,同样是由于错误15914。


总而言之,A::C问题是一个开放的核心语言问题,但ICC在这里实现了语言的预期含义。其他编译器在访问检查和模板方面都存在问题。