【问题标题】:Member specialization of alias declaration in different namespaces不同命名空间中别名声明的成员特化
【发布时间】:2017-10-02 21:41:04
【问题描述】:

我刚刚在 clang 和 gcc 之间遇到了一个奇怪的行为差异,我想编译类似于以下内容的代码:

namespace n1 {
  template <class T1, class T2>
  struct MyTemplate {
    struct Inner {};
  };
}

using namespace n1;
namespace n2 {
  using MyClass = MyTemplate<int, int>;
}

namespace n1 {
  using n2::MyClass;
  template<> struct MyClass::Inner {
    int member;
  };

  MyClass::Inner inner{0};
}

Clang 愉快地编译了这个:

$ clang++ -std=c++11 -c -o alias_specialization.o alias_specialization.cc

但是 gcc 抛出以下错误:

$ g++ -std=c++11 -c -o alias_specialization.o alias_specialization.cc

alias_specialization:15:30: error: declaration of ‘struct n1::MyTemplate<int, int>::Inner’ in namespace ‘n1’ which does not enclose ‘using MyClass = struct n1::MyTemplate<int, int>’
   template<> struct MyClass::Inner {

我知道我可以在第 15 行写原始类型的全名 (MyTemplate&lt;int, int&gt;) 而不是 MyClass。 但我只是想知道这两个编译器中哪一个是“正确的”。 使用的确切编译器是:

$ clang++ --version
clang version 4.0.0

$ g++ --version
g++ (GCC) 6.3.1 20170306

【问题讨论】:

  • 我不知道 GCC 是否正确,但请注意,如果您在 n1 中“导入”MyClass,则可以通过编写 using MyClass = n2::MyClass; 而不是 using n2::MyClass; 来欺骗!

标签: c++ c++11 gcc clang clang++


【解决方案1】:

免责声明GCC Clang 是正确的(正如@suluke 指出的那样,使用下面的调查结果)。

我在 C++14 标准中找到了以下段落,但我确信这同样适用于 C++11:

第一部分:模板的专业化和实例化

inline namespace(§7.3.1 cl. 8):

内联命名空间的成员在大多数情况下都可以使用,就好像它们是封闭命名空间的成员一样。具体来说,内联命名空间及其封闭的命名空间都被添加到参数相关查找 (3.4.2) 中使用的关联命名空间集合中,只要其中一个是,以及一个命名内联命名空间的 using- 指令 (7.3.4)与未命名的命名空间 (7.3.1.1) 一样,隐式插入到封闭的命名空间中。此外,内联命名空间的每个成员随后都可以部分特化 (14.5.5)、显式实例化 (14.7.2) 或显式特化 (14.7.3),就好像它是封闭的命名空间。最后,通过显式限定 (3.4.3.2) 在封闭命名空间中查找名称将包括由 using 指令引入的内联命名空间的成员,即使在封闭命名空间中有该名称的声明。

显式实例化(§ 14.7.2 cl. 3)

如果显式实例化用于类或成员类,则声明中的详细类型说明符应包含一个简单模板标识。如果显式实例化是针对函数或成员函数,则声明中的 unqualified-id 应为模板 ID,或者在可以推导出所有模板参数的情况下,为模板名称或操作符函数 ID。 [注意:声明可以声明一个qualified-id,在这种情况下qualified-id的unqualified-id必须是一个template-id。 —尾注] 如果显式实例化是针对类模板特化的成员函数、成员类或静态数据成员,则成员名称的限定 ID 中的类模板特化的名称应为 simple-template -ID。如果显式实例化是针对变量的,则声明中的 unqualified-id 应为 template-id。 显式实例化应出现在其模板的封闭命名空间中。如果显式实例化中声明的名称是非限定名称,则显式实例化应出现在声明其模板的名称空间中,或者,如果该名称空间是内联的(7.3.1),则应出现在其封闭名称空间集中的任何名称空间中。

显式实例化(§ 14.7.2 cl. 6)

类、函数模板或变量模板特化的显式实例化放置在定义模板的命名空间中。类模板成员的显式实例化放置在定义封闭类模板的命名空间中。成员模板的显式实例化放置在定义封闭类或类模板的命名空间中。 [ 例子:

namespace N {
    template<class T> class Y { void mf() { } };
  }
  template class Y<int>;  // error: class template Y not visible 
                          // in the global namespace

  using N::Y;
  template class Y<int>; // error: explicit instantiation outside of the 
                         // namespace of the template


  template class N::Y<char*>;  // OK: explicit instantiation in namespace N
  template void N::Y<double>::mf();  // OK: explicit instantiation
                                     // in namespace N

—结束示例]

第二部分:Typedef,别名

什么是 typedef 或别名?

§7.1.3 分类。 1 个状态:

[...] 使用 typedef 说明符声明的名称成为 typedef-name。在其声明的范围内,typedef-name 在语法上等同于关键字,并以第 8 节中描述的方式命名与标识符相关联的类型。因此 typedef-name 是另一种类型的同义词。 typedef-name 不会像类声明 (9.1) 或枚举声明那样引入新类型。

§7.1.3 分类。 2 个状态:

typedef-name 也可以由 alias-declaration 引入。 using 关键字之后的标识符成为 typedef-name,标识符之后的可选属性说明符序列属于该 typedef-name。 它与由 typedef 说明符引入的语义相同。特别是,它没有定义新的类型,也不会出现在 type-id 中。

第三部分:测试您的案例

按照您的逻辑,以下代码应编译并生成 2 种不同的类型。我用 Clang 测试了它,这是有问题的编译器。 Clang 生成的类型与其所需的标准类型相同。

#include <iostream>

namespace n1 {
  template <class T1, class T2>
  struct MyTemplate {
    struct Inner {};
  };
}

using namespace n1;
namespace n2 {
  using MyClass = MyTemplate<int, int>;
}

namespace n3 {
  using MyClass = MyTemplate<int, int>;
}

namespace n1 {
  using n2::MyClass;
  template<> struct MyClass::Inner {
    int member;
  };

  MyClass::Inner inner{0};
}

namespace n4{
  using n3::MyClass;

  MyClass::Inner inner{0};
}

int main()
{
  using namespace std;

  cout << typeid(n1::inner).name() << endl;
  cout << typeid(n4::inner).name() << endl;

  return 0;
}

编译

c++ -std=c++14 typedef-typeid.cpp -O3 -o compiled.bin

c++ -std=c++11 typedef-typeid.cpp -O3 -o compiled.bin

两种情况下的输出

N2n110MyTemplateIiiE5InnerE
N2n110MyTemplateIiiE5InnerE

事实证明,在这种情况下typedefusing 是等价的,我们可以使用typedef 代替using

§7.3.3 分类。 1 指出了这一点:

[...] 如果 using 声明命名了一个构造函数(3.4.3.1),它会在出现 using 声明的类中隐式声明一组构造函数(12.9); 否则在 using 声明中指定的名称是另一个命名空间或类中的一组声明的同义词

#include <iostream>
#include <typeinfo>

namespace n1 {
  template <class T1, class T2>
  struct MyTemplate {
    struct Inner {};
  };
}

using namespace n1;
namespace n2 {
  typedef MyTemplate<int, int> MyClass;
}


namespace n1 {
  typedef n2::MyClass MyClass;

  template<> struct MyClass::Inner {
    int member;
  };

  MyClass::Inner inner{0};
}

int main()
{
  using namespace std;

  cout << typeid(n1::inner).name() << endl;

  return 0;
}

而且 GCC 完美地编译了这个例子,没有任何责备。

此外,了解带有 using 声明的 传递性 如何与 GCC 一起工作会很有趣。

这是修改后的例子:

#include <iostream>
#include <typeinfo>

namespace n1 {
  template <class T1, class T2>
  struct MyTemplate {
    struct Inner {};
  };
}


using namespace n1;
namespace n2 {
  using MyClass = MyTemplate<int, int>;

}

namespace n3
{
  using n2::MyClass;
}


namespace n1 {
  typedef n3::MyClass MyClass;

  template<> struct MyClass::Inner {
    int member;
  };

  MyClass::Inner inner{0};
}

int main()
{
  using namespace std;

  cout << typeid(n1::inner).name() << endl;

  return 0;
}

Clang 和 GCC 再次愉快地编译它。

【讨论】:

  • 这当然是一个复杂的话题,如果我忽略了什么,请原谅。但我认为让我的示例有点特别的是类型别名MyClass,它有点模糊了定义MyClass::Inner 的命名空间。所以我的理解是 gcc 将类型别名视为 #define 而 clang 将其视为一种新类型。我在您的答案中没有看到任何对类型别名的引用,所以我认为这部分仍然缺失。仍然非常感谢您花时间研究标准!
  • 是的,感谢您指出。我重写了我的答案,使其更有条理,并通过额外的测试解决了缺失的部分。
  • 好的,我接受了你的回答,但现在我认为这是错误的:D 如果我们采用 > An explicit instantiation shall appear in an enclosing namespace 和 > A typedef-name is thus a synonym for another type 和 > [using] has the same semantics as if it were introduced by the typedef specifier 那么我相信 clang 是对的而 gcc 是错的. template&lt;&gt; struct MyClass::Inner 对于编译器来说应该和template&lt;&gt; struct MyTemplate&lt;int, int&gt;::Inner 完全一样,对吧?正如@tux3 的评论所暗示的,using 声明(“import”)可能是这里的问题......
  • 如果您只是更改答案的第一行,我可以再次将其标记为已接受的答案 :) 我说“导入”时的意思可能是问题是 using n2::Myclass 的使用与第 14 行中的 using MyClass = n2::Myclass 相反。虽然我真的认为这不重要(但请随时再次查看标准;))。让我们同意 gcc 在这里是错误的
  • 我只是查了一下,并采用了第 7.3.3 节中的附加引用和另一个示例的答案,这表明 GCC 有多么错误:)
猜你喜欢
  • 2014-07-19
  • 2015-08-04
  • 2017-06-01
  • 2020-01-14
  • 2018-07-24
  • 2011-03-02
  • 1970-01-01
  • 2011-12-10
  • 1970-01-01
相关资源
最近更新 更多