GCC不喜欢与匿名命名空间的前向声明交朋友,但MSVC喜欢。什么意思?

10

这段代码在 MSVC 上编译通过,但在 GCC 上不行,在 GodBolt.org 测试时出现该问题。

Baz 类在匿名命名空间中声明,这会导致 GCC 认为它是一个与下面我定义的不同的类,而 MSVC 似乎将它们连接起来了。

namespace { class Baz; }

class Foo { protected: int x; };
class Bar : public Foo { friend class Baz; };

namespace {
  class Baz { void f() { Bar b; b.x = 42; } };
}

按照标准来说,什么是正确的呢?


1
@给踩票者,我在思考这是一个好问题时错过了什么? - Bathsheba
1
@Jeffrey,我在问题中的godbolt链接中加入了代码。本来应该从一开始就这样做的。 - Macke
1
你可以摆脱继承,只需使用 class Bar { friend class Baz; protected: int x; };。我仍然可以用它来复现 - 在 msvc 和 clang 中编译通过,但在 gcc 中不行。 - Kevin
2
这个问题可能与您相关:https://dev59.com/tWIj5IYBdhLWcg3wy4DH - 1201ProgramAlarm
1
嗯,clang编译它很好,所以我认为这是gcc的问题。看起来像Bug 71882 - user7860670
显示剩余2条评论
3个回答

5
这似乎是语言措辞上的不一致,不同的编译器在这个问题上有不同的立场。MSVC和clang将接受原始代码,但像GCC和Edge这样的编译器则会拒绝它。
冲突的措辞来自于:
10.3.1.2 [namespace.memdef]
...用于确定实体是否已被先前声明的查找不得考虑最内层封闭命名空间之外的任何作用域。 结构体Baz没有在最内层封闭命名空间中声明,但在那里是可见的,因此正常的名称查找会找到它。但由于这不是正常的名称查找,像gcc和Edge这样的编译器不会查找封闭的命名空间,只会查找最内层的命名空间。
这些信息来自于gcc bug,其中讨论了这个主题。
看起来MSVC和Edge选择以不同的方式解释使用匿名命名空间,这将转换OP的代码为以下内容:
namespace unnamed { }
using namespace unnamed;
namespace unnamed { struct Baz; }

class Foo { protected: int x; };
class Bar : public Foo { friend class Baz; };

namespace unnamed { class Baz { void f() { Bar b; b.x = 42; } }; }

由于不同的解释,像gcc和Edge这样的编译器也会拒绝此等效代码,但由于MSVC和clang认为通过using声明或指令引入的类型是否被视为friend名称查找,因此它们被接受。有关此问题的更多信息,请参见cwg-138


编译器如gcc和Edge选择不继续查找封闭的命名空间,而MSVC和Edge会。嗯?没有人会查看封闭的命名空间,问题是:查找是否应该穿过使用指令。匿名NS不包含全局NS。 - Language Lawyer
我不评论任何一种解释的正确性,我只是指出了不一致之处,正如链接的gcc错误报告中所提到的那样。此外,在你发表评论的15秒前,我还编辑了我的回答,其中包括GCC无法通过using-指令找到friend名称查找的事实,这与你提到的使用指令有关的不一致性相关。 - Human-Compiler
我在你发表评论之前15秒编辑了我的回答,但我仍然看到有关封装命名空间的句子。 - Language Lawyer
这是因为我的评论几乎与Jonathon Wakely在一个几乎相同的示例的错误报告中所说的话完全一致。 - Human-Compiler

4
问题在于您在友元声明中使用了一个详细的类型说明符,GCC将其用于在全局命名空间中声明类 Baz 详细的类型说明符是一个声明除非在最内层的命名空间中找到先前的声明。 显然不清楚是否应该将 Baz 的声明视为在全局命名空间中。
要解决此问题,只需在友元声明中使用类的名称即可:
namespace { class Baz; }

class Foo { protected: int x; };
class Bar : public Foo { friend Baz; };

namespace {
  class Baz { void f() { Bar b; b.x = 42; } };
}

在友元声明中使用详细类型说明符是一种惯用的病态习惯。除非类型的名称也是变量的名称,否则没有理由使用详细类型说明符。

1
我今天才知道我一直在使用详细的类型说明符来进行友元声明。我之前并不知道这一点。但是,我想如果要在类模板之间进行友元声明,仍然需要使用详细的说明符,对吗? - Human-Compiler
如果查找没有找到前面的声明,您的代码将在全局命名空间中声明一个名为Baz的类。问题是,查找是否应该通过using指令进行。 - Language Lawyer
@LanguageLawyer 已修复。 - Oliv
1
详细的类型说明符是一个声明,除非在最内层包围命名空间中找到了先前的声明。所以 GCC 是错误的,这并不完全清楚。 - Language Lawyer
@LanguageLawyer 好的,这个歧义没有任何未解决的问题吗? - Oliv
太棒了!感谢您教授我们有关详细类型说明符的知识! - Macke

2

匿名命名空间的作用就像它们有一个唯一的名称,并且仅对当前的翻译单元可用。

看起来有些编译器会给翻译单元中的所有匿名命名空间相同的名称,而其他编译器可能不会(这只是一种可能的实现猜测),但似乎不能依赖它。

有关匿名命名空间的更多详细信息,请参见此处: https://en.cppreference.com/w/cpp/language/namespace#Unnamed_namespaces


我不确定这是否与此相关。如果您将初始声明更改为 namespace { class Baz { }; }(定义 Baz),则 gcc 将会给出“重定义'class {anonymous}::Baz'”错误,因此两个匿名命名空间被视为相同的空间。 - 1201ProgramAlarm
根据您提供的链接,似乎是未指定的。另外,根据上面的错误链接,EDG和GCC走了一条路,Clang和MSVC走了另一条路。当然啦。 :) - Macke
@1201ProgramAlarm 另一方面,由于第一个后面隐含着“using <anon1>”,因此在第二个匿名命名空间中,当前查找已经具有来自第一个匿名命名空间的Baz,因此冲突是有道理的。 - Macke

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