jtbandes
jtbandes

Reputation: 118771

When are incomplete types okay during explicit instantiation?

I'm trying to make a kind of wrapper class which automatically creates a wrapped object:

#include <memory>
#include <type_traits>

template<typename T>
class Foo {
    std::unique_ptr<T> _x;
public:
    Foo();  // will initialize _x
};

Furthermore, I want the ability to hide the implementation details of T from users of Foo<T> (for the PIMPL pattern). For a single-translation-unit example, suppose I have

struct Bar;  // to be defined later

extern template class Foo<Bar>;
// or just imagine the code after main() is in a separate translation unit...

int main() {
    Foo<Bar> f;  // usable even though Bar is incomplete
    return 0;
}

// delayed definition of Bar and instantiation of Foo<Bar>:

template<typename T>
Foo<T>::Foo() : _x(std::make_unique<T>()) { }

template class Foo<Bar>;
struct Bar {
    // lengthy definition here...
};

This all works fine. However, if I want to require that T derives from another class, the compiler complains that Bar is incomplete:

struct Base {};

template<typename T>
Foo<T>::Foo() : _x(std::make_unique<T>()) {
    // error: incomplete type 'Bar' used in type trait expression
    static_assert(std::is_base_of<Base, T>::value, "T must inherit from Base");
}

An attempt to achieve the same check using static_cast fails similarly:

template<typename T>
Foo<T>::Foo() : _x(std::make_unique<T>()) {
    // error: static_cast from 'Bar *' to 'Base *', which are not related by inheritance, is not allowed
    // note: 'Bar' is incomplete
    (void)static_cast<Base*>((T*)nullptr);
}

However, it seems if I add another level of function templating, I can make this work:

template<typename Base, typename T>
void RequireIsBaseOf() {
    static_assert(std::is_base_of<Base, T>::value, "T must inherit from Base");
}

// seems to work as expected
template<typename T>
Foo<T>::Foo() : _x((RequireIsBaseOf<Base, T>(), std::make_unique<T>())) { }

Note that even the following still causes an incomplete-type error despite the similar structure:

// error: incomplete type 'Bar' used in type trait expression
template<typename T>
Foo<T>::Foo() : _x((std::is_base_of<Base, T>::value, std::make_unique<T>())) { }

What is going on here? Does the additional function somehow delay the checking of the static_assert? Is there a cleaner solution that doesn't involve adding a function, but still allows placing template class Foo<Bar>; before the definition of Bar?

Upvotes: 4

Views: 1166

Answers (1)

GeckoGeorge
GeckoGeorge

Reputation: 445

Version 1

// #1
// POI for Foo<Bar>: class templates with no dependent types are instantiated at correct scope BEFORE call, with no further lookup 
// after first parse
int main() {
    Foo<Bar> f;  // usable even though Bar is incomplete
    return 0;
}

// delayed definition of Bar and instantiation of Foo<Bar>:


struct Base {};

// error: incomplete type 'Bar' used in type trait expression
template<typename T>
Foo<T>::Foo() : _x(std::make_unique<T>()) {
    // error: incomplete type 'Bar' used in type trait expression
    static_assert(std::is_base_of<Base, T>::value, "T must inherit from Base");
}
// #2
// POI for static_assert: function templates with no dependent types are
// instantiated at correct scope AFTER call, but no further lookup is
// performed, as with class templates without dependent types
// is_base_of forces the compiler to generate a complete type here

template class Foo<Bar>;
struct Bar : private Base {
    // lengthy definition here...
};

version 2:

    struct Base {};
template<typename Base, typename T>
void RequireIsBaseOf() {
    static_assert(std::is_base_of<Base, T>::value, "T must inherit from Base");
}

// seems to work as expected
template<typename T>
Foo<T>::Foo() : _x((RequireIsBaseOf<Base, T>(), std::make_unique<T>())) { }
// #3
// is_base_of does not force any complete type, as so far, only the 
// incomplete type of RequiredIsBaseOf is around.

template class Foo<Bar>;
struct Bar : private Base {
    // lengthy definition here...
};
// #3
// POI for RequiredIsBaseOf: function templates WITH dependent types are instantiated at correct scope AFTER call, after the second phase of two-phase lookup is performed. 

Here is the rub in my opinion: Any point after #2 is an allowed POI (Point Of Instantiation, where the compiler puts the specialized template code) according to the rules.

In practice, most commpilers delay the actual instantiation of most function templates to the end of the translation unit. Some instantiations cannot be delayed, including cases where instantiation is needed to determine a deduced return type and cases where the function is constexpr and must be evaluated to produce constant result. Some compilers instantiate inline functions when they're first used to potentially inline the call right away. This effectively removes the POIs of the corresponding template specializations to the end of the translation unit, which is permitted by the C++ standard as an alternative POI (from C++ Templates, The Complete Guide, 2nd Ed., 14.3.2. Points of Instantiation, p.254)

std::is_base_of requires a complete type, so when it's not hidden by RequiredIsBaseOf, which can as function template be partially instantiated, is_base_of can lead compilers who insert POIs as soon as possible to issue an error.

As noted by t.niese, any version of gcc on godbolt that can take the -std=c++17 flag is fine with either version. My guess is that gcc does one of the late POI things, while clang uses the first legal one, #2. The use of a function template with dependent names (when RequiredIsBaseOf is first encountered, T still has to be filled in) forces clang to do a second lookup run for the dependent type, by which time Bar has been encountered.

I'm not sure how to actually verify this though, so any input from people more versed in compilers would be welcomed.

Upvotes: 1

Related Questions