3
votes
#include <iostream>
template<typename T>
struct A{
    template<typename U>
    struct B{};
    B<T> b;   //#2
};
//#3
int main() {
    A<int> a;  // #1
}

Consider the above code, the use of the template-id A<int> causes an implicit instantiation for specialization A<int>. According to the following rule:

temp.point#4

For a class template specialization, a class member template specialization, or a specialization for a class member of a class template, if the specialization is implicitly instantiated because it is referenced from within another template specialization, if the context from which the specialization is referenced depends on a template parameter, and if the specialization is not instantiated previous to the instantiation of the enclosing template, the point of instantiation is immediately before the point of instantiation of the enclosing template. Otherwise, the point of instantiation for such a specialization immediately precedes the namespace scope declaration or definition that refers to the specialization.

For the specialization referenced at #1, the Otherwise part of that rule will apply to it. That means, the point of instantiation for A<int> should at #3, there's no issue here. However, the instantiation for A<int> will cause the instantiation for specialization B<int> which is a member thereof. Hence, the if part of that rule will apply to B<int>. What I'm confused about is here. According to the relevant rule, the point of instantiation for B<int> shall be immediately before the point of instantiation of the enclosing template, that is, somewhere before #3, I can't understand here. If, think it through the normal class, there're only two ways to define its class member, one is to define the class member in the class definition, the other is to declare the class member in the class definition and then define the class member outside the class definition at some point where follows the classes definition.

Change the example by using a normal class:

struct Normal_A_Int{
   struct Normal_B_Int{};
   Normal_B_Int b;
};
int main(){
  Normal_A_Int a;
}

That means, the definition for member class Normal_B_Int must be in the definition of the enclosing class due to the declaration Normal_B_Int b is required a complete class type.

So, how is it possible to let the definition for member class B<int> be placed at some point precede the definition for an enclosing class A<int>? At best, the definition for B<int> shall be in the definition for A<int>. How to interpret the POI for the first example?

2
Is it specified anywhere that "point of instantiation" means "placing definition at that point"?Language Lawyer
@LanguageLawyer The standard does not explicitly say that, however, the rule temp.inst#10 together with [temp.point#4] hint that.xmh0511
@dfrib Yes, for function specializations, there are two POI(one is specified by the rule, the other is the end of TU), however, for class specialization, there's only one POI which is specified by the relevant rule. IMHO, POI should be understood in English, that is, where the instantiation invented, Right?xmh0511
Yes I now see that [temp.point]/8 was even clarified (now as [temp.point]/7) in the draft, explicitly pointing out that a specialization of a class template has at most one POI within a TU.dfrib
Ah, @DavisHerring is the author of P1787! Maybe he can say if I reflected things accurately in my wall of text answer.Jeff Garrett

2 Answers

4
votes

The quote you provided from temp.point/4 is exactly the case:

template<typename T>
struct A {
    template<typename U>
    struct B { };
    B<T> b;
};

// point-of-instantiation for A<int>::B<int>
// point-of-instantiation for A<int>
int main() {
    A<int> a;
}

There's not much lawyering to do. The standard says those are the points of instantiation. Class templates are not classes and intuition doesn't necessarily carry over. They have both a definition and instantiation.

Take the outer class definition. If it is a class, the member's type must be defined. If it is a class template, the member's type must only be declared. You can lower the definition of B:

template<typename T>
struct A {
    template<typename U> struct B;
    B<T> b;
};

template<typename T>
template<typename U>
struct A<T>::B { };

You could not do this with classes (have an "incomplete" member in the definition), but you can with class templates.

So the question is, why is the point-of-instantiation of template A<T>::B<T> before that of A<T>? Ultimately, because the standard says so. But consider that if it were after, you couldn't have an inner class template at all. And if it were, say, inside the definition of A<T>, name lookup would misbehave because the names between the definition of A and the point of instantiation of A<int> would not be visible in A<int>::B<int>. So it actually makes some sense.

Perhaps the intuition comes from conflating definition and point-of-instantiation:

So, how is it possible to let the definition for member class B be placed at some point precede the definition for an enclosing class A?

It isn't. The point-of-instantiation controls name visibility. All names up to that point in the TU are visible. (It is not the definition.) From this point of view, it is clear intuitively that A<int>::B<int> should have a point-of-instantiation near to the point-of-instantiation of A<int> (they should see the same other names). If there is an ordering, probably the inner should come first (so that A<int> can have a A<int>::B<int> member). If there is not an ordering, there has to be language about how the instantiations are interleaved or interact.

There are two interesting aspects of this rule.

One is that templates can be specialized. So, to accomplish this requirement, when the compiler goes to instantiate A<int>, it must first select the specialization, process it enough to know it has a member class template and that it needs to instantiate it, and then stop everything to go instantiate A<int>::B<int> first. That is not difficult, but it is subtle. There is an instantiation stack, as it were.

The second aspect is far more interesting. You may expect from ordinary class intuition that the definition of B<T> can use things (e.g. typedefs) from A<T> that would in a template context require an instantiation of A<T> which doesn't yet exist when A<T>::B<T> is being instantiated. Like:

template<typename T>
struct A {
    using type = T;

    template<typename U>
    struct B { using type = typename A<U>::type; };

    B<T> b;
};

Is that OK?

If the point-of-instantiation of A<int>::B<int> is before that of A<int>, we cannot really form A<int>::type.

This is the realm of CWG287 and P1787.

CWG287 proposed that the point-of-instantiations be the same (one is not before the other). Further, it would add:

If an implicitly instantiated class template specialization, class member specialization, or specialization of a class template references a class, class template specialization, class member specialization, or specialization of a class template containing a specialization reference that directly or indirectly caused the instantiation, the requirements of completeness and ordering of the class reference are applied in the context of the specialization reference.

In my example, A<int>::B<int> references A<int> which directly caused its instantiation by way of reference to B<int>, so the requirements of completeness and ordering of the class reference (typename A<int>::type) are applied in the context of the specialization reference (B<int> b). So it is OK. It is still OK if the typedef is below the definition of B. But if we move the typedef below the member b, it will be ill-formed! Subtle! This has the effect of interleaving the instantiations. When we see the member, we stop what we're doing, go off to instantiate A<int>::B<int>, but we can use the ordering and completeness requirements from where we were in the instantiation of A<int>. The point-of-instantiation is the same, so we can also use the same declarations from the TU.

CWG287 seems to aim at replicating what compilers do already. However, CWG287 has been open since 2001. (See also this and this.)

P1787 which seems to be targeted at C++23, aims to rewrite a lot of subtle language. I think aims to have similar effect as CWG287. But to do so, they had to so thoroughly re-define name lookup that it is very difficult to me to know that. :)