Kirill Kobelev
Kirill Kobelev

Reputation: 10557

Accessing template parameters of the member templates

My first code fragment compiles and works fine:

template <class x> struct B1
{
    template <class x> struct B2     { int getX() { return(16); } };
    template <class x> struct B2<x*> { int getX() { return(20); } };
};

void main(int argc, char* argv[])
{
    B1<int>::B2<int>  a1;
    B1<int>::B2<int*> a2;
    printf("a1=%d, a2=%d.\r\n", a1.getX(), a2.getX());
}

Note that the name of template param is x in both templates and this is not confusing the compiler. The second example fails with the compiler crash (MSVC 2008), i.e. without giving any syntax error:

template <class x> struct B1
{
    template <class x> struct B2 { int getX() { return(16); } };
    template <class x> struct B2<x*>;
};

template <class x> template <class x>
struct B1<x>::B2<x*>
{
    int getX() { return(3); }
};

My question is not on how to fix this example, this is obvious.

I looked in the standard (C++2003) on the rules of accessing params of template headers and I cannot find anything relevant. Maybe I am missing something.

While processing B1<x>, should compiler consider only params from the first template header? More likely yes. Then while processing B2<x>, should it consider both? More complex case is especially interesting when params of B1/B2 contain qualified identifiers themselves and these identifiers want to access params of the template header. For simplicity I am not giving the full example for this.

If anybody came across this and can comment or know some artickles/books, YouTube videos, etc, I would love to hear this.

UPDATE: I just tried the following with MSVC:

template <class x> struct B1
{
    x qq1;

    struct B2
    {
        int x;
    };
};

This compiles and works as expected. I also tried an exe that accesses the inner data field x. This shows that MS compiler implemented hiding template params in the inner scopes. For me this is logical even if this does not comply with the standard.

Upvotes: 3

Views: 2795

Answers (1)

jogojapan
jogojapan

Reputation: 69967

Although the question has been considered answered by the comments, I'll provide some further details below. Note that my answer is based on C++11 (ISO/IEC 14882-2011) only.

Part 1: Can a template parameter be re-used as template-parameter of a nested member template?

There are two key statements in the standard about the status of template parameters (specifically, type-parameters – which are the only kind relevant to the question). The first statement describes them as having the same status as typedef-names:

(§14.1/3) A type-parameter whose identifier does not follow an ellipsis defines its identifier to be a typedef-name (if declared with class or typename) or template-name (if declared with template) in the scope of the template declaration. [...]

And regarding the possible redeclaration of typedef-names, we have:

(§7.1.3/6) In a given scope, a typedef specifier shall not be used to redefine the name of any type declared in that scope to refer to a different type. [...]

(Remark: The rule above seems to apply to the use of typedef specifiers only, although alias declarations (§7.1.3/2) and template parameter declarations (§14.1/3) can be used to declare typedef-names as well. That is, the rule above does not explicitly rule out the use of an alias declaration or indeed a template parameter to redeclare a typedef-name within the same scope, although that is clearly the intended meaning. The wording should be "no typedef-name declaration shall be used" instead of "a typedef specifier shall not be used".)

What it means is that you cannot do this:

{
  typedef int   T;
  typedef float T;
}

because the second declaration occurs in the same scope in which T was originally declared. However, this:

{
  typedef int   T;
  {
    typedef float T;
  }
}

is perfectly legally according to the rule above, because the second declaration is in a block scope which (although the first T is still valid there) is not the scope in which T was originally declared.

Due to §14.1/3 quoted above, we must assume that the rule applies to template parameter declarations as well, hence something like

template <typename X> template <typename X>
struct Outer<X>::Inner<X> {

};

is illegal even on the simple basis that it implies the declaration of the same typedef-name twice within the same scope.

However, in a case like

template <typename X>
struct Outer {

  template <typename X>
  struct Inner {
  };

};

one may argue that the second declaration of template <typename X> applies to a nested scope. Fortunately, the standard does provide the following second statement about the status of template parameters:

(§14.6.1/6) A template-parameter shall not be redeclared within its scope (including nested scopes). A template-parameter shall not have the same name as the template name. [Example:

template<class T, int i> class Y {
  int T; // error: template-parameter redeclared

  void f() {
    char T; // error: template-parameter redeclared
  }
};

template<class X> class X; // error: template-parameter redeclared

— end example ]

As clearly stated, the no-redeclaration rule that applies to any typedef-name within the declaration scope, applies to nested scopes as well in the case of template parameters.

Here is an example to motivate why I think that rule is actually useful, too. Consider:

template <typename T1>
struct Outer
{
  static const int outerID = 5;

  template <typename T2>
  struct Inner
  {
    int id1() { return Outer<T1>::outerID; }
    int id2() { return Outer::outerID; }
    int id3() { return outerID; }
  };
};

The three functions of the inner template all refer to the same static member of the outer class, but in three different ways. id2() and id3() do this because §14.6.2.1/4 requires Outer::outerID and outerID to be interpreted as referring to the current instantiation, which is Outer<T1>::outerID.

If we now replace the template parameter of the inner template with T1, the same as for the outer template, the meaning of id1() would change (because Outer<T1> would now refer to whatever the definition of T1 in the inner template is), but id2() and id3() would – most naturally – still refer to the current instantiation of the template outerID belongs to. Therefore, id1() may return a different value than id2() and id3(), which would be most awkward.

Part 2: In a partial specialization of a member template, can template parameters of the member be used as template arguments of the enclosing class, and vice versa?

Another problem addressed by the question is whether in a specialization or outside-of-class definition of a member template, such as

template <typename A> template <typename B>
struct Outer<A>::Inner<B> {
  // ...
};

the template argument list of the outer template (i.e. <A> in this case) could make use of parameters defined for the inner template (i.e. B in this case), and vice versa.

Let's first consider the special case given in the question, where the two parameters are identical:

template <typename A> template <typename A>
struct Outer<A>::Inner<A> {
  // ...
};

Although we have ruled this out in Part 1 already due to the re-declaration problem, we can still consider the intended meaning of this syntax, which is: Define an explicit specialization of the inner class template, in which the template argument is assumed to be same as for the outer template. The syntactically correct way to write that is

template <typename A> template <>
struct Outer<A>::Inner<A> {
  // ...
};

i.e. the second parameter list would be empty. Unfortunately, this would amount to an explicit specialization of a member template, which is illegal unless the enclosing template is also explicitly specialised (§14.7.3/16).

However, if we consider a partial, rather than an explicit specialization of a member template with two or more parameters, the declaration becomes legal:

// Primary declaration
template <typename A>
struct Outer {
  template <typename A, typename B>
  struct Inner {
    // ...
  };
};

// Partial specialization of member template
template <typename A> template <typename B>
struct Outer<A>::Inner<B,A> {
  // ...
};

We have now used the template argument A of the enclosing template as specialised second argument for the inner template as well. Any instantiation of the template that uses the same data type for the template argument of the outer class as well as the second template argument of the inner class, e.g.

Outer<int>::Inner<float,int> myvar;

will instantiate the specialization defined above.

Hence, using template parameters of an enclosing class in the template argument list of a member template is without problems, and the reason is that by the time Inner<B,A> is evaluated, A already has the status of a typedef-name defined at the scope level of Outer.

But doing it reversely, e.g.

template <typename A> template <typename B>
struct Outer<B>::Inner<B,A> {
  // ...
};

will not work, because B is a typedef-name of the Inner scope only. The Standard states:

(§14.5.2/1) [...] A member template of a class template that is defined outside of its class template definition shall be specified with the template-parameters of the class template followed by the template-parameters of the member template. [Example:

    template<class T> struct string {
      template<class T2> int compare(const T2&);
      template<class T2> string(const string<T2>& s) { /∗ ... ∗/ }
    };
    template<class T> template<class T2> int string<T>::compare(const T2& s) {
    }

— end example ]

I interpret this to mean that the two template parameter lists (one followed by the other) are kept separately, not considered as one combined list of template parameters. Hence in

template <typename A> template <typename B>
struct Outer<B>::Inner<B,A>

the interpretation of Outer<B> cannot make use of the second template parameter list, and B will be undefined, whereas in the previous case

template <typename A> template <typename B>
struct Outer<A>::Inner<B,A>

the interpretation of Inner<B,A> is possible because A has the status of a typedef-name declared in a scope that Inner is part of, i.e. it will be successfully interpreted as Inner<B,Outer::A>, which in turn is the same as Inner<B,Outer<A>::A>.

Upvotes: 4

Related Questions