K
kanze
Think of it this way. In your first example, the argument for T (let's
call it arg) is looked up in the scope of the instantiation. Then, the
scope of the definition of f is loaded. The equivalent of:
typedef arg T;
is executed, which declares T to be of type arg, and the T is
installed in the scope of instantiation. Then, the semantic analysis
of f happens in the scope of the instantiation. T is found using
normal lookup rules. It works just like a function call does, except
that the "parameter passing" into the definition scope happens at
compile time rather than run time. The analogous happens with Base in
the second example.
So in sum, symbols inside the template body are looked up in the context
of the point of instantiation, and not in the context of the point of
definition. To make it clearer:
extern void g( int ) ;
template< typename T >
void
f( T const& t )
{
g( t ) ; // #1
g( 1.5 ) ; // #2
}
User code, in another module... (I'm supposing we've done whatever is
necessary to use the f, above here. In C++, we've included the header
where f is defined. In D, I don't know what is necessary.)
void g( double ) {}
void
h()
{
f( 2.5 ) ; // #3
}
In the instantiation of f triggered by line #3, which g is called in
line #1? In line #2? If symbols are looked up in the context of the
definition, both lines should call g(int), because that is the only
symbol g visible at the point of instantiation. If symbols are looked
up at the point of instantiation (as is the case in traditional C++,
e.g. CFront, early Borland, etc.), then both lines call g(double). The
two phase look-up in standard C++ means that line #1 calls g(double),
and line #2 g(int).
C++ tries to emulate this behavior with the two-phase lookup
rules. But it's hashed up - did you know you cannot declare a local
with the same name as a template parameter? It follows weird rules
like that that are all its own.
There are, IMHO, two problems with two phased look-up as it is
implemented in C++. The first is that which names are dependant, and
which aren't, is determined by fairly subtle rules -- even when quickly
scanning f, above, you might miss the fact that the two g's are looked
up in entirely different contexts, and this is an extremely simple
case. (I'm not sure but what I wouldn't have prefered an explicit
declaration -- a name is only dependant if the template author
explicitly says it is.) The second is the fact that the name lookup is
not only in a different context, but follows subtly different rules.
I don't have enough experience with compilers implementing two phase
lookup correctly to say whether this will be a real problem in actual
practice. It may turn out to be like function overload resolution: the
rules are so complicated that almost no pratitioner begins to really
understand them, but in real code, provided a few, common sense rules
are followed, the results are what one would intuitively expect, and
there is no problem. I hope this is the case, but I'm not counting on
it. (Part of the problem, of course, is that many pratitioners already
have concrete experience with a different set of rules, which works
differently. Just being aware that two phase lookup exists, and knowing
a few simple rules to force dependency when you want it, goes a long
way.)