When Instantiating a Template, Should Members of Its Incomplete Argument Types Be Visible

When instantiating a template, should members of its incomplete argument types be visible?

Whether typename T::Before is valid is not explicitly said by the spec. It is subject of a defect report (because the Standard can very reasonably be read to forbid it): http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#287 .

Whether typename T::After is invalid can also very reasonably be read to be true by the spec, and actually it makes quite a bit of sense (and aforementioned DR still keeps it ill-formed). Because you have an instantiation of a class A<Foo>, which references another class A<Bar> during a period where a member Baz has not yet been declared, and that makes a reference back to A<Foo>::Bar. That is ill-formed in the case of non-templates aswell (try to "forget" for a moment that you are dealing with templates: surely the lookup of B<A>::After is done after the A template was completely parsed, but not after the specific instantiation of it was completely created. And it is the instantiation of it that actually will do the reference!).

struct A {
typedef int Foo;
typedef A::Foo Bar; // valid
typedef A::Baz Lulz; // *not* valid
typedef int Baz;
};

Point of instantiation of a template class

Your example is not identical to the one in the defect report. In the defect report, CL is a class template. However the intent of the proposed resolution is to make the template case the same as the non-template one, aka [basic.scope.pdecl]:

6 After the point of declaration of a class member, the member name
can be looked up in the scope of its class. [ Note: this is true
even if the class is an incomplete class. For example,

struct X {
enum E { z = 16 };
int b[X::z]; // OK
};

end note ]

Then the proposed resolution:

In 14.6.4.1 [temp.point] paragraph 3 change:

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.

To:

the point of instantiation is the same as the point of
instantiation of the enclosing template. Otherwise, the point of
instantiation for such a specialization immediately precedes the
nearest enclosing declaration. [Note: The point of instantiation is still at namespace scope but any declarations preceding the point of
instantiation, even if not at namespace scope, are considered to have
been seen.]

Add following paragraph 3:

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.

As of the latest draft, the non-template case was and is still valid. The template case is not. However the defect is drafting, which means that the template case is intended to compile.

Drafting: Informal consensus has been reached in the working group and is described in rough terms in a Tentative Resolution, although
precise wording for the change is not yet available.

Template specialization doesn't see a function in its point of instantiation

Here the argument t of fnc is a dependent name, which cannot be resolved when parsing templates. Instead, they are looked up again at the point of instantiation. This is so-called two-phase lookup: the 1st phase is the parsing of a template, and the 2nd phase is its instantiation.

The answer to your question lies in the fact that the 2nd lookup performed at the POI (point of instantiation) is only an ADL. Because the struct CL is not defined inside the same namespace with void f(CL), the POI lookup would therefore not take place and would not find it.

If you try to put the definition of the struct CL into the namespace to make ADL take effect, it will compile well.

namespace NS {
struct CL{};
void f(CL){}
//...
}

According to the rule of unqualified name lookup, (bold by me)

For a dependent name used in a template definition, the lookup is
postponed until the template arguments are known, at which time ADL
examines function declarations with external linkage (until C++11)
that are visible from the template definition context as well as in
the template instantiation context, while non-ADL lookup only examines
function declarations with external linkage (until C++11) that are
visible from the template definition context (in other words, adding
a new function declaration after template definition does not make it
visible except via ADL
).

Inheriting from template instanciated with incomplete type

Using S<A> as base class causes it to be implicitly instantiated.

Generally that wouldn't be a problem since your class doesn't require A to be complete when it is instantiated.

However, the definition of your destructor for S<A> requires A to be complete (because of the member access). That would normally also not be a problem, because generally definitions of member functions are not implicitly instantiated with implicit instantiation of the class template specialization, but only when they are used in a context requiring a definition to exist or in the case of the destructor when it is potentially invoked.

However, your destructor is virtual. For virtual member functions in particular it is unspecified whether they are instantiated with implicit instantiation of the containing class. ([temp.inst]/11)

So, the implementation may or may not choose to instantiate S<A>::~S<A> in the translation unit for main.cpp. If it does, the program will fail to compile since the member access in the definition is ill-formed for an incomplete type.

In other words, it is unspecified whether or not the program is valid and all mentioned compilers behave conforming to the standard.

If you remove virtual (and override) on the destructor(s) the only instantiation of the S<A> destructor allowed will be in the ConcreteS.cpp translation unit where A is complete and instantiation valid. The program is then valid and it should compile under MSVC as well.

gcc and clang implicitly instantiate template arguments during operator overload resolution

The entire point of the matter is ADL kicking in:

N3797 - [basic.lookup.argdep]

When the postfix-expression in a function call (5.2.2) is an unqualified-id, other namespaces not considered
during the usual unqualified lookup (3.4.1) may be searched, and in those namespaces, namespace-scope
friend function or function template declarations (11.3) not otherwise visible may be found.

following:

For each argument type T in the function call, there is a set of zero or more associated namespaces and a
set of zero or more associated classes to be considered. [...] The sets of
namespaces and classes are determined in the following way:

  • If T is a class type [..] its associated classes are: ...
    furthemore if T is a class template specialization its associated namespaces and classes also include: the namespaces and classes associated with the
    types of the template arguments provided for template type parameters

D<A> is an associated class and therefore in the list waiting for its turn.

Now for the interesting part [temp.inst]/1

Unless a class template specialization has been explicitly instantiated (14.7.2) or explicitly specialized (14.7.3),
the class template specialization is implicitly instantiated [...] when the completeness of the class type affects the semantics of the program

One could think that the completeness of the type D<A> doesn't affect at all the semantic of that program, however [basic.lookup.argdep]/4 says:

When considering an associated namespace, the lookup is the same as the lookup performed when the associated namespace is used as a qualifier (3.4.3.2)
except that:

[...]
Any namespace-scope friend functions or friend function templates declared in associated classes are visible within their respective
namespaces even if they are not visible during an ordinary lookup (11.3)

i.e. the completeness of the class type actually affects friends declarations -> the completeness of the class type therefore affects the semantics of the program.
That is also the reason why your second sample works.

TL;DR D<A> is instantiated.

The last interesting point regards why ADL starts in the first place for

u = v; // Triggers ADL
u.operator=(v); // Doesn't trigger ADL

§13.3.1.2/2 dictates that there can be no non-member operator= (other than the built-in ones). Join this to [over.match.oper]/2:

The set of non-member candidates is the result of the unqualified lookup of operator@ in the context
of the expression according to the usual rules for name lookup in unqualified function calls (3.4.2)
except that all member functions are ignored.

and the logical conclusion is: there's no point in performing the ADL lookup if there's no non-member form in table 11. However [temp.inst]p7 relaxes this:

If the overload resolution process can determine the correct function to call without instantiating a class template definition, it is unspecified whether that instantiation actually takes place.

and that's the reason why clang triggered the entire ADL -> implicit instantiation process in the first place.

Starting from r218330 (at the time of writing this, it has been committed a few minutes ago) this behavior was changed not to perform ADL for operator= at all.


References

  • r218330
  • clang sources / Sema module
  • cfe-dev
  • N3797

Thanks to Richard Smith and David Blaikie for helping me figuring this out.

C++ why templating a class resolves undefined class error

In the case of A::Anested, A is an incomplete type when a is declared, the compiler hasn't seen the whole declaration of A yet, so it can't declare a as an instance of A. Incomplete types only work when dealing with references and pointers.

In the case of B::Bnested, templates are handled in multiple stages. The compiler knows that B<n> exists when b is declared, but it does not know the actual value of n yet, so it does not instantiate b yet. When later code instantiates an instance of B<n> with an actual argument for n, the compiler will then know the complete type of B<n>, and can instantiate b with that same type.

Why the error in this simple template code?

At the point of the static_assert, foo isn't yet a complete type (you're still within its definition), so when bar tries to reach in, the compiler gives you an error. The particular error it gives you sucks though; try it with a different compiler.

Curiously Recurring Template and Template parameter dependent subclassing issues

Normally, if you want A to inherit from B, then B cannot know anything about A other than it's declaration:

template < class __object >
struct Derived;

Unfortunately, you want to get more, so you'll have to use a type trait:

template<class __derived>
struct Base_traits {
//using Object = ?????;
};
template<class __object>
struct Base_traits<Derived<__object>> {
using Object = __object; //note, this also can't inspect B.
};

The Base class can inspect the Base_traits all it wants, because the traits don't inspect B at all.

template < class __derived, class __object = typename Base_traits<__derived>::Object >
struct Base {
using Derived = __derived;
using Object = typename Base_traits<__derived>::Object;
//or
using Object = __object;


Unrelated, leading double underscores are disallowed for use by mortals, use a single leading underscore followed by a lowercase letter. Alternatively, use a trailing underscore.

Also, the syntax

void function(Object o) { return Derived::function(s); }

Won't work, because that notation cannot be used for upcasts, only downcasts. Ergo, you have to use static_cast on the this. Since that's vaguely ugly, I put it behind a function:

    void foo(Object o) { self()->bar(o); }
private:
__derived* self() {return static_cast<__derived*>(this);}
const __derived* self() const {return static_cast<__derived*>(this);}
};

Full code: http://coliru.stacked-crooked.com/a/81595b0fcd36ab93



Related Topics



Leave a reply



Submit