Is instantiating a class template with an incomplete type ill-formed, if the type is defined afterwards?

  • A+
Category:Languages

This code is surely ill-formed, because Foo is specialized after an instantiation point:

template <typename T> struct Foo {     int a; };  Foo<int> x = { 42 };  template <> struct Foo<int> {     const char *a; };  Foo<int> x = { "bar" }; 

It is ill formed because of the part of the standard I've put emphasis:

A specialization for a function template, a member function template, or of a member function or static data member of a class template may have multiple points of instantiations within a translation unit, and in addition to the points of instantiation described above, for any such specialization that has a point of instantiation within the translation unit, the end of the translation unit is also considered a point of instantiation. A specialization for a class template has at most one point of instantiation within a translation unit. A specialization for any template may have points of instantiation in multiple translation units. If two different points of instantiation give a template specialization different meanings according to the one-definition rule, the program is ill-formed, no diagnostic required.

Now, is this code ill-formed?

struct A;  template <typename> class Foo { };  Foo<A> foo; // note A is incomplete here  struct A {}; 

Does the ill-formedness change, if Foo declared like this?

struct A;  template <typename T> struct Foo {     Foo() {         new T;     } };  Foo<A> foo; // note A is incomplete here  struct A {}; 

I asked this question, because of the discussion under this question.

Note, this is not a duplicate. That question is about why the code compiles, this question is about whether it is ill-formed. They differ, because an ill-formed program isn't necessarily a non-compiling program.


Note, with clang and gcc, my example with new T compiles, while this example (T as a member) doesn't:

struct A;  template <typename T> struct Foo {     T t; };  Foo<A> foo; // note A is incomplete here  struct A {}; 

Maybe both are ill-formed, and diagnostic is given only for this last case?

 


struct A; template <typename> class Foo { }; Foo<A> foo; // note A is incomplete here struct A {}; 

Foo<A> only depends on the name of A not its complete type.

So this is well-formed; however, this kind of thing can still break (become ill-formed) yet compile in every compiler you test.

First, we steal is_complete. Then we do this:

struct A; template <class T> class Foo {   enum{ value = is_complete<T>::value }; }; Foo<A> foo; // note A is incomplete here struct A {}; 

We are ok, despite this:

[...] for any such specialization that has a point of instantiation within the translation unit, the end of the translation unit is also considered a point of instantiation. [...]

because that clause does not apply to template classes. Here, the only instantiation of the template class is fine.

Now, if in another file you have:

struct A {}; Foo<A> foo2; 

then you are ill-formed.

In addition, the one-file case:

struct A; template <class T> class Foo {   enum{ value = is_complete<T>::value }; }; Foo<A> foo; // note A is incomplete here struct A {}; Foo<A> foo2; // ill-formed 

is also ill-formed.

Both will almost certainly compile in C++ compilers with no errors or warnings. Most compilers memoize template instantiations; Foo<A> will have a ::value that is false even after foo2 is created (with a complete A). But that is just a symptom of ill-formed, no diagnostic required programs (a step more extreme than UB).


Finally, note that many types in std require that their template arguments are complete. As an example, std::vector<T> requires T to be complete.

Most implementations of std::vector<T> are fine with an incomplete T until you try to use a method (including many of its constructors or the destructor), but the standard states that T must be complete.

This actually gets in the way of some unuseful code, like having a function type that returns vectors of its own type1. Boost has a library to solve this problem.


template <typename T> struct Foo {   Foo() {     new T;   } }; 

The body of Foo<T>::Foo() is only instantiated "when called". So T's lack of completion has no impact until Foo::Foo() is called.

Foo<A> foo; 

^^ will fail to compile with a non-complete A.

using foo_t = Foo<A>; 

^^ will compile, and cause no problems.

using foo_t = Foo<A>; struct A {}; foo_t foo; 

also no problems. The body of foo_t::foo_t gets instantiated when we try to construct a foo_t, and all definitions match.


1 Can you say state machine transition function?

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: