Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Member specialization of alias declaration in different namespaces

I just encountered a strange difference in behavior between clang and gcc where I wanted to compile code which looks similar to the following:

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 happily compiles this:

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

but gcc throws the following error:

$ 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 {

I know I can just write the full name of the original type (MyTemplate<int, int>) instead of MyClass in line 15. But I'm simply wondering which of the two compilers is "right". The exact compilers in use are:

$ clang++ --version
clang version 4.0.0

$ g++ --version
g++ (GCC) 6.3.1 20170306
like image 905
suluke Avatar asked May 04 '17 15:05

suluke


1 Answers

Disclaimer: GCC Clang is right (as @suluke pointed out, using the findings below).

I found the following passages in the C++14 standard, but I am sure the same applies to C++11:

Part I: Specialization & Instantiation of Templates

inline namespace (§7.3.1 cl. 8):

Members of an inline namespace can be used in most respects as though they were members of the enclosing namespace. Specifically, the inline namespace and its enclosing namespace are both added to the set of associated namespaces used in argument-dependent lookup (3.4.2) whenever one of them is, and a using- directive (7.3.4) that names the inline namespace is implicitly inserted into the enclosing namespace as for an unnamed namespace (7.3.1.1). Furthermore, each member of the inline namespace can subsequently be partially specialized (14.5.5), explicitly instantiated (14.7.2), or explicitly specialized (14.7.3) as though it were a member of the enclosing namespace. Finally, looking up a name in the enclosing namespace via explicit qualification (3.4.3.2) will include members of the inline namespace brought in by the using-directive even if there are declarations of that name in the enclosing namespace.

Explicit instantiation (§ 14.7.2 cl. 3)

If the explicit instantiation is for a class or member class, the elaborated-type-specifier in the declaration shall include a simple-template-id. If the explicit instantiation is for a function or member function, the unqualified- id in the declaration shall be either a template-id or, where all template arguments can be deduced, a template-name or operator-function-id. [Note: The declaration may declare a qualified-id, in which case the unqualified-id of the qualified-id must be a template-id. —end note] If the explicit instantiation is for a member function, a member class or a static data member of a class template specialization, the name of the class template specialization in the qualified-id for the member name shall be a simple-template-id. If the explicit instantiation is for a variable, the unqualified-id in the declaration shall be a template-id. An explicit instantiation shall appear in an enclosing namespace of its template. If the name declared in the explicit instantiation is an unqualified name, the explicit instantiation shall appear in the namespace where its template is declared or, if that namespace is inline (7.3.1), any namespace from its enclosing namespace set.

Explicit instantiation (§ 14.7.2 cl. 6)

An explicit instantiation of a class, function template, or variable template specialization is placed in the namespace in which the template is defined. An explicit instantiation for a member of a class template is placed in the namespace where the enclosing class template is defined. An explicit instantiation for a member template is placed in the namespace where the enclosing class or class template is defined. [ Example:

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

— end example ]

Part II: Typedef, Aliasing

What is typedef or an alias?

§7.1.3 cl. 1 states:

[...] A name declared with the typedef specifier becomes a typedef-name. Within the scope of its declaration, a typedef-name is syntactically equivalent to a keyword and names the type associated with the identifier in the way described in Clause 8. A typedef-name is thus a synonym for another type. A typedef-name does not introduce a new type the way a class declaration (9.1) or enum declaration does.

§7.1.3 cl. 2 states:

A typedef-name can also be introduced by an alias-declaration. The identifier following the using keyword becomes a typedef-name and the optional attribute-specifier-seq following the identifier appertains to that typedef-name. It has the same semantics as if it were introduced by the typedef specifier. In particular, it does not define a new type and it shall not appear in the type-id.

Part III: Testing Your Case

Following your logic the following code should compile and produce 2 different types. I tested it with Clang, which is the compiler in question. Clang produces the same type as standard it requires.

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

Compilation

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

OR

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

Output in both cases

N2n110MyTemplateIiiE5InnerE
N2n110MyTemplateIiiE5InnerE

As it turns out that in this context typedef and using are equivalent, we can use typedef instead of using.

§7.3.3 cl. 1 points this out:

[...] If a using-declaration names a constructor (3.4.3.1), it implicitly declares a set of constructors in the class in which the using-declaration appears (12.9); otherwise the name specified in a using-declaration is a synonym for a set of declarations in another namespace or class.

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

And GCC perfectly compiles the example, without blaming anything.

In addition it'd be interesting to find out how the transitiveness with using declaration works with GCC.

Here is the modified example:

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

And again both Clang and GCC happily compile it.

like image 107
ovanes Avatar answered Sep 18 '22 14:09

ovanes