template <bool X>
struct C : public B<X> {
// using B<X>::x; // OK
using A<X>::x; // Why OK?
C() { x = 1; }
};
The question is why wouldn't that be supported? Because the constrain that A<X>
is a base of a specialization of the main template definition of C
is a question that can only be answered, and that only makes sense for a particular template argument X
?
To be able to check templates at definition time was never a design goal of C++. Many well formed-ness constrains are checked at instanciation time and this is fine.
[Without a true concept (necessary and sufficient template parameter contracts) support no variant of C++ would do significantly better, and C++ is probably too complicated and irregular to have true concepts and true separate checking of templates, ever.]
The principles that makes it necessary to qualify a name to make it dependent does not have anything with early diagnostic of errors in template code; the way name lookup works in template was considered necessary by the designers to support "sane" (actually slightly less insane) name lookup in template code: a use of a non local name in a template shouldn't bind too often to a name declared by the client code, as it would break encapsulation and locality.
Note that for any unqualified dependent name you can end up accidentally calling an unrelated clashing user function if it's a better match for overloading resolution, which is another problem that would be fixed by true concept contracts.
Consider this "system" (i.e. not part of current project) header:
// useful_lib.hh _________________
#include <basic_tool.hh>
namespace useful_lib {
template <typename T>
void foo(T x) { ... }
template <typename T>
void bar(T x) {
...foo(x)... // intends to call useful_lib::foo(T)
// or basic_tool::foo(T) for specific T
}
} // useful_lib
And that project code:
// user_type.hh _________________
struct UserType {};
// use_bar1.cc _________________
#include <useful_lib.hh>
#include "user_type.hh"
void foo(UserType); // unrelated with basic_tool::foo
void use_bar1() {
bar(UserType());
}
// use_bar2.cc _________________
#include <useful_lib.hh>
#include "user_type.hh"
void use_bar2() {
bar(UserType()); // ends up calling basic_tool::foo(UserType)
}
void foo(UserType) {}
I think that code is pretty realistic and reasonable; see if you can see the very serious and non local issue (an issue that can only be found by reading two or more distinct functions).
The issue is caused by the use of an unqualified dependent name in a library template code with a name that isn't documented (intuitivement shouldn't have to be) or that is documented but that the user wasn't interested in, as he never needed to override that part of the library behavior.
void use_bar1() {
bar(UserType()); // ends up calling ::foo(UserType)
}
That wasn't intended and the user function might have a completely different behavior and fails at runtime. Of course it could also have an incompatible return type and fail for that reason (if the library function returned a value unlike in that example, obviously). Or it could create an ambiguity during overload resolution (more involved case possible if the function takes multiple arguments and both library and user functions are templates).
If this wasn't bad enough, now consider linking use_bar1.cc and use_bar2.cc; now we have two uses of the same template function in different contexts, leading to different expansions (in macro-speak, as templates are only slightly better than glorified macros); unlike preprocessor macros, you are not allowed to do that as the same concrete function bar(UserType)
is being defined in two different ways by two translation units: this is an ODR violation, the program is ill formed no diagnostic required. That means that if the implementation doesn't catch the error at link time (and very few do), the behavior at runtime is undefined from start: no run of the program has defined behavior.
If you are interested, the design of name lookup in template, in the era of the "ARM" (Annotated C++ Reference Manual), long before ISO standardization, is discussed in D&E (Design and Evolution of C++).
Such unintentional binding of a name was avoided at least with qualified names and non dependent names. You can't reproduce that issue with a non dependent unqualified names:
namespace useful_lib {
template <typename T>
void foo(T x) { ... }
template <typename T>
void bar(T x) {
...foo(1)... // intends to call useful_lib::foo<int>(int)
}
} // useful_lib
Here the name binding is done such that no better overload match (that is no match by a non template function) can "beat" the specialization useful_lib::foo<int>
because the name is bound in the context of the template function definition, and also because useful_lib::foo
hides any outside name.
Note that without the useful_lib
namespace, another foo
that happened to be declared in another header included before could still be found:
// some_lib.hh _________________
template <typename T>
void foo(T x) { }
template <typename T>
void bar(T x) {
...foo(1)... // intends to call ::foo<int>(int)
}
// some_other_lib.hh _________________
void foo(int);
// user1.cc _________________
#include <some_lib.hh>
#include <some_other_lib.hh>
void user1() {
bar(1L);
}
// user2.cc _________________
#include <some_other_lib.hh>
#include <some_lib.hh>
void user2() {
bar(2L);
}
You can see that the only declarative difference between the TUs is the order of inclusion of headers:
user1
causes the instanciation of bar<long>
defined without foo(int)
visible and name lookup of foo
only finds the template <typename T> foo(T)
signature so binding is obviously done to that function template;
user2
causes the instanciation of bar<long>
defined with foo(int)
visible so name lookup finds both foo
and the non template one is a better match; the intuitive rule of overloading is that anything (function template or regular function) that can match less argument lists wins: foo(int)
can only match exactly an int
while template <typename T> foo(T)
can match anything (that can be copied).
So again the linking of both TUs causes an ODR violation; the most likely practical behavior is that which function is included in the executable is unpredictable, but an optimizing compiler might assume that the call in user1()
does not call foo(int)
and generate a non inline call to bar<long>
that happens to be the second instanciation that ends up calling foo(int)
, which might cause incorrect code to be generated [assume foo(int)
could only recurse through user1()
and the compiler sees it doesn't recurse and compile it such that recursion is broken (this can be the case if there is a modified static variable in that function and the compiler moves modifications across function calls to fold successive modifications)].
This shows that templates are horribly weak and brittle and should be used with extreme care.
But in your case, there is no such name binding issue, as in that context a using declaration can only name a (direct or indirect) base class. It doesn't matter that the compiler cannot know at definition time whether it's a direct or indirect base or an error; it will check that in due time.
While early diagnostic of inherently erroneous code is permitted (because sizeof(T())
is exactly the same as sizeof(T)
, the declared type of s
is illegal in any instantiation):
template <typename T>
void foo() { // template definition is ill formed
int s[sizeof(T) - sizeof(T())]; // ill formed
}
diagnosing that at template definition time is not practically important and not required for conforming compilers (and I don't believe compiler writers try to do it).
Diagnostic only at the point of instantiation of issues that are guaranteed to be caught at that point is fine; it does not break any design goal of C++.