This code is surely ill-formed, because Foo
is specialized after an instantiation point:
template
struct Foo {
int a;
};
Foo
struct A;
template class Foo { };
Foo foo; // note A is incomplete here
struct A {};
Foo
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 Foo {
enum{ value = is_complete::value };
};
Foo 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 foo2;
your program is ill formed.
However, in the one-file case:
struct A;
template class Foo {
enum{ value = is_complete::value };
};
Foo foo; // note A is incomplete here
struct A {};
Foo foo2; // ill-formed
your code is fine. There is one point of instantiation for Foo
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
will have a ::value
that is false
even after foo2
is created (with a complete A
). Others will have two different Foo
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 c++11: “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
used to require T
to be complete.
By c++17 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 c++17, most implementations of std::vector
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
struct Foo {
Foo() {
new T;
}
};
The body of Foo
is only instantiated "when called". So T
's lack of completion has no impact until Foo::Foo()
is called.
Foo foo;
^^ will fail to compile with a non-complete A
.
using foo_t = Foo;
^^ will compile, and cause no problems.
using foo_t = Foo;
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?