12
votes

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?

3
If this were ill-formed NDR, vector<Incomplete> would also be for the same reason. But we can have vectors of incomplete types just fine.Barry
An explicit specialization is not a POI of anything. Your first snippet is ill-formed by [temp.expl.spec]/6.T.C.
@T.C.: I meant Foo<int> x = { "bar" }; as POI. Foo<int>'s meaning is different there than at Foo<int> x = { 42 };. Isn't this correct thinking?geza
@Barry (and T.C.): thanks!geza

3 Answers

7
votes
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;

your program is ill formed.

However, in 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

your code is fine. There is one point of instantiation for Foo<A> in a given compilation unit; the second one is a reference to the first point of instantiation.

Both the one and two file versoins will almost certainly compile in C++ compilers with no errors or warnings.

Some compilers memoize template instantiations even from one compilation unit to anther; Foo<A> will have a ::value that is false even after foo2 is created (with a complete A). Others will have two different Foo<A>s in each compilation unit; its methods will be marked inline (and be different), the size of the classes may disagree, and you'll get cascades of ill formed program problems.


Finally, note that many types in std require that their template arguments are complete in older versions of C++ (including : “17.6.4.8 Other functions (...) 2. the effects are undefined in the following cases: (...) In particular - if an incomplete type (3.9) is used as a template argument when instantiating a template component, unless specifically allowed for that component” -- copied from boost incomplete container docs). To be concrete, std::vector<T> used to require T to be complete.

By that has changed for std::vector:

[vector.overview]/3

An incomplete type T may be used when instantiating vector if the allocator satisfies the allocator completeness requirements 17.6.3.5.1. T shall be complete before any member of the resulting specialization of vector is referenced.

Now, even prior to , 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 stated 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?

4
votes

Assuming we only have one translation unit, [temp.point] rules out your quote as a possible source of ill-formedness

A specialization for a class template has at most one point of instantiation within a translation unit.

Instead, the problem with the first snippet is [temp.expl.spec]

If a template, a member template or a member of a class template is explicitly specialized then that specialization shall be declared before the first use of that specialization that would cause an implicit instantiation to take place, in every translation unit in which such a use occurs; no diagnostic is required.

The second snippet is well-formed, there is no requirement that template parameters need to have complete type.

The third snippet is ill-formed, new T requires that T be a complete type. A slight catch here is that the definition of the constructor is implicitly instantiated at Foo<A> foo;. If however, the snippet is changed to

struct A;

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

using FooA = Foo<A>;

struct A {};

Then the definition of the constructor isn't instantiated and will therefore be well-formed. [temp.inst]

The implicit instantiation of a class template specialization causes

  • the implicit instantiation of the declarations, but not of the definitions, of the non-deleted class member functions, member classes, scoped member enumerations, static data members, member templates, and friends; and [...]

The fourth snippet is ill-formed because members need to have complete type. [class.mem]

The type of a non-static data member shall not be an incomplete type [...]

1
votes

Fortunatly, yes this is well-defined. For the exact same reason this is well-defined:

struct A;

class Foo { A* value; };

Foo foo; // note A is incomplete here

struct A {};

and this is ill-formed:

struct A;

template <class T> class Foo { T value; }; // error: 'Foo<T>::value' has incomplete type

Foo<A> foo; // note A is incomplete here

struct A {};