C++ template name lookup rules
When defining template class or function template, knowing how these template parameters are instantiated during compiling time is important to correctly use them. This article tries to explain(but with no full coverage) the order behind the name look up. The template parameter name look up is complex. This article only cover the commonly used cases. For a full understanding of the issue, please refer to official documentation or C++ ISO standard.
- Types of template parameters
- The base rule: “Two Phase Name Lookup”
- Rules for non template dependent names
- Rules for template dependent names
- Current instantiation
Types of template parameters
Each parameter in parameter-list may be:
- a non-type template parameter;
- a type template parameter;
- a template template parameter.
non-type template parameters
-
template parameter can be non-type
template<const int* pci> struct X {}; int ai[10]; X<ai> xi; // OK: array to pointer conversion and cv-qualification conversion
type template parameter
template parameters without name
- template parameter name is optional!!
template template parameter
template<typename T>
class my_array {};
// two type template parameters and one template template parameter:
template<typename K, typename V, template<typename> typename C = my_array>
class Map
{
C<K> key;
C<V> value;
};
The base rule: “Two Phase Name Lookup”
When the compiler parses the template relevant code, it resolves the names in the template in two stages:
- First at the template definition
- Second at the instantiation of the template
Here is an example of this basic rule:
EXAMPLE_#1
:
#include <iostream>
template <typename T> struct SomeStruct {
void f() { std::cout << "Hello\n" << sizeof(T) << '\n'; }
};
template <typename T> struct DerivedStruct : public SomeStruct<T> {
void h() {
// Nok, because f() is non-template dependent, so name lookup happens
// at phase one, but during this time there is no f() defined,
// because SomeStruct has not been instantiated yet
// f();
// Ok, because 'this' is template dependent, f() lookup happens at
// phase two, at this time SomeStruct has been instantiated and f() is
// available
this->f();
}
};
int main(int argc, char const *argv[]) {
DerivedStruct<int>().h();
return 0;
}
As in above example, there are differences when name look up and definition binding for f()
and this->f()
. That is because they belong to two different name types:
- Dependent names: or template dependent names. These are names that can be different for different template parameter types(type template parameter) or parameter values(non-type template parameter). In this example,
this->f()
is template dependent, because,this
can beDerivedStruct<int>
or it can beDerivedStruct<double>
- Non-dependent names: or non template dependent names. These are names that is always the same between different instantiations. In this example
f()
is non template dependent name, because whether isDerivedStruct<int>
orDerivedStruct<double>
, the call forf()
is the same. It has nothing to do with parameterT
Rules for non template dependent names
Non template dependent names are determined at stage one, namely, template definition.
Here is an example to demonstrate this behaviour:
EXAMPLE_#2
:
#include <iostream>
struct FooStruct;
template <typename T> struct SomeStruct {
// Since f is non template dependent, it will be looked up and bound here
// But at this time FooStruct has not been defined(only declaration), the
// compiler does not know how much memory f will take, so it gives incomplete
// type error
FooStruct f;
};
struct FooStruct {};
int main(int argc, char const *argv[]) {
// Even though at the time of instantiation, the FooStruct is fully defined
// SomeStruct does not compile because name look up and bound of member f
// happens at template definition time
SomeStruct<int>();
return 0;
}
Another example of non template dependent name look up behaviour:
EXAMPLE_#3
:
#include <iostream>
void f(double data) { std::cout << "Double type recieved\n"; }
template <typename T> struct SomeStruct {
// Here f is already looked up and bound with f(double data)
void some_method() { f(3); }
};
// Even there is a more matched version of f(int data), it will not be called
void f(int data) { std::cout << "Int type recieved\n"; }
int main(int argc, char const *argv[]) {
// This will call f(int data), because it is more compatible
f(3);
// This will call f(double data), because even there is more compatible
// version, the f is looked up and bound before f(int data) is available
SomeStruct<int>().some_method();
return 0;
}
- In above example, the
f(int data)
is not visible toSomeStruct
Rules for template dependent names
The normal case
The behaviour is clear when compared with following example with EXAMPLE_#2
EXAMPLE_#4
, which makes the f
template dependent in EXAMPLE_#2
. Now the code compiles:
#include <iostream>
template <typename T> struct FooStruct;
template <typename T> struct SomeStruct {
// Since f is template dependent, it will be looked up and bound not at here
// but at the instantiation; Even though here FooStruct is not defined yet
// it does not effect the compile
FooStruct<T> f;
};
template <typename T> struct FooStruct {};
int main(int argc, char const *argv[]) {
// Here the FooStruct is fully defined, code compiles
SomeStruct<int>();
return 0;
}
Different behaviour when using non-ADL and ADL
Firstly a basic understanding of non-ADL
and ADL
should be required, please refer to Argument-dependent lookup - cppreference.com for detailed info. For an extremely simplified explanation of what ADL
does is that:
- When look up a function name, not only in current namespace, but also the namespace of the arguments are added to the look up scope
- For fundamental types, no additional argument namespace is added
Here is an example to demonstrate this behaviour:
EXAMPLE_#5
#include <iostream>
void f(double data) { std::cout << "Double passed in\n"; }
template <typename T> struct FooStruct;
template <typename T> struct SomeStruct {
// Since f is template dependent, it will be looked up and bound not at here
// but at the instantiation; Even though here FooStruct is not defined yet
// it does not effect the compile
FooStruct<T> f;
};
template <typename T> struct FooStruct {
FooStruct() {
T t;
// Here f(..) name look up depends on type of T:
// 1. If non-ADL look up, for example T is of fundamental types(int,
// double..) the look up scope ends here, it's look up is based on template
// definition context; Functions declared after this template definition are
// not visible.
// 2. If ADL look up, for example T is of user defined types, the look up
// scope will include the namespace where T resides. The look up scope is
// based on template definition context or template instantiate context,
// which means that functions declared before the instantiation is also
// visible.
f(t);
}
};
void f(int data) { std::cout << "Int passed in\n"; }
struct UserType {};
void f(UserType data) { std::cout << "UserType passed in\n"; }
int main(int argc, char const *argv[]) {
// Here whether it's int or double, void f(int data) is not visible to f(t)
// in FooStruct constructor. It's a non-ADL look up.
SomeStruct<int>();
SomeStruct<double>();
// Here since UserType is user defined, it's ADL look up. The look up scope is
// template definition scope or instantiation scope, which means void
// f(UserType data) is visible.
SomeStruct<UserType>();
return 0;
}
Output:
Double passed in
Double passed in
UserType passed in
This behaviour can be summarized in one sentence:
For template dependent names, adding a new function declaration after template definition does not make it visible, except via ADL(from cppreference.com)
Why template definition context when non-ADL?
The reason is to not violate the ODR
(One Definition Rule).
To demonstrate this, let’s make the template a header library. File SomeStruct.h
:
// SomeStruct.h
#include <iostream>
inline void f(double data){};
template <typename T> struct FooStruct;
template <typename T> struct SomeStruct {
FooStruct<T> f;
};
template <typename T> struct FooStruct {
FooStruct() {
T t;
f(t);
}
};
Now we have two translation unit that both use the SomeStruct.h
library, respectively TU_a.cpp
and TU_b.cpp
. And they have namespace A
and namespace B
respectively.
File TU_a.cpp
:
#include "SomeStruct.h"
#include <iostream>
namespace A {
void f(double data) { std::cout << "Double passed in, at namespace A\n"; }
SomeStruct<double> a();
} // namespace A
File TU_b.cpp
:
#include "SomeStruct.h"
#include <iostream>
namespace B {
void f(double data) { std::cout << "Double passed in, at namespace B\n"; }
SomeStruct<double> b();
} // namespace B
Let’s analysis what will happen if we compile and link the two translation unit:
- Since
T
isdouble
, it’s a non-ADL name look up. Let’s see what will happen if we use template instantiation context for name look up:TU_a.cpp
will useA::f
TU_b.cpp
will useB::f
- Both
TU_a.cpp
andTU_b.cpp
instantiate the same data typeSomeStruct<double>
, but they have multi definition off
, which clearly violates theODR
- Both
- So instead of using template instantiation context when non-ADL name look up, compiler use template definition context for name look up, which leads to:
inline void f(double data){};
is use for bothTU_a.cpp
andTU_b.cpp
Why template instantiation context when ADL?
The key point is:
- When using ADL look up, the type is user defined, such as
SomeStruct<UserType_A>
orSomeStruct<UserType_B>
, the types which are instantiated can not be the same - But with fundamental types, above assertion is not true. Some different translation unit can instantiate the same type, such as
SomeStruct<double>
in the example. To keep theODR
rule, it has to use the template definition context for name look up, not template instantiation context.
Current instantiation
Names that belong to current instantiation means that their look up and binding can be done based on current instantiation of the template, no further instantiation is required to compete look up and binding. Following is an example to demonstrate this:
#include <iostream>
template <typename T> struct FooStruct {
// FooStruct<T> is current instantiation because when instantiate this
// template FooStruct<T> is itself, no additional instantiation is required to
// determine FooStruct<T>
FooStruct<T> *ptr;
// FooStruct<T*> is NOT current instantiation because when instantiate this
// template FooStruct<T>, another instantiation which is FooStruct<T*> is
// required.Note: here is an compiling error
FooStruct<T *> ptr_ptr;
// Current instantiation, no further instantiations are required
T c;
T *c_ptr;
};
typename
for template dependent types
A name that is not a member of current instantiation and is dependent on template argument is not considered a type, except using typename
keyword:
Here is an example to demonstrate this:
#include <iostream>
template <typename T> struct BarStruct {
typedef T *ptr_type;
template <typename U> struct NestedStruct {
NestedStruct() { std::cout << "Hello\n"; }
};
};
template <typename T> struct FooStruct {
// Here ptr_type is not a current instantiation and a member of FooStruct,
// typename has to be used to indicate that it's a type
typename BarStruct<T>::ptr_type ni;
};
int main(int argc, char const *argv[]) { FooStruct<double> b; }
template
for template dependent templates
A name is not considered a template if the name is not a member of current instantiation and depend on template argument, except using template
keyword:
#include <iostream>
template <typename T> struct BarStruct {
typedef T *ptr_type;
template <typename U> struct NestedStruct {
NestedStruct() { std::cout << "Hello\n"; }
};
};
template <typename T> struct FooStruct {
// Here NestedStruct<T> is at the same time template and type name; typename
// and template keywords must be used
typename BarStruct<T>::template NestedStruct<T> com;
};
int main(int argc, char const *argv[]) { FooStruct<double> b; }
Output:
Hello