Search code examples
c++templatestype-traitsstatic-assertincomplete-type

Why does this static_assert on a pointer to an incomplete type in a templated function apparently work?


In the process of replacing a static_assert in a templated function with a requires statement, we discovered that firstly, the function was occasionally being used on an incomplete type, and that secondly, this apparently compiled. The version with the requires statement, of course, is illegal.

Here's a simple version of the apparently-functioning static_assert:

#include <type_traits>

struct Bar {};

template<typename T>
inline Bar* AsBar(T* ptr)
{
    static_assert(std::is_convertible_v<T*, Bar*>);
    return (Bar*)ptr;
}

struct Foo;

void DoThing(Foo* f)
{
    AsBar(f);
}

struct Foo : public Bar {};

This surprises me! My model of how the templating works is that at the point of instantiation, all the code is generated, and at that point we invoked std::is_convertible_v on an incomplete type, which is undefined behaviour. Sticking a non-templated version of the same function at that point fails.

So I have some questions:

Firstly, does the templated static_assert that is working for an incomplete type have defined behaviour? Or are we simply 'getting lucky' with undefined behaviour?

Secondly, if it's defined, why? What is incomplete about my understanding of templates?


Solution

  • tldr; The end of a translation unit is considered a point of instantiation, at which point Foo is complete. Additionally, a pointer type is a complete-type.


    Firstly, is the templated static_assert working for an incomplete type defined behavior?

    A pointer type is a complete-type because the size of the pointer object does not depend on the size of the pointed-to type

    The following types are incomplete types:

    • the type void (possibly cv-qualified);
    • incompletely-defined object types:
      • class type that has been declared (e.g. by forward declaration) but not defined;
      • array of unknown bound;
      • array of elements of incomplete type;
      • enumeration type from the point of declaration until its underlying type is determined.

    Note none of the bullet points are satisfied/applicable for the pointer type in your example.


    Perhaps I’m missing something, but why doesn’t the static assert fail here? I agree the pointer type is a complete type, but it shouldn’t be convertible to Bar*

    Additionally, the point of instantiation of a function template is also at the end of a translation unit where Foo is complete. Can the point of instantiation be delayed until the end of the translation unit?


    A c-tagged question Why are pointers to incomplete types allowed and not variables of incomplete types?