4

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?

jtbandes
  • 115,675
  • 35
  • 233
  • 266
  • Hmm, your first example with `static_assert(std::is_base_of::value, "T must inherit from Base");` seems to work fine for me with gcc 5.5, 6.3, 7.3, 8.1, 9 but reports the incomplete error for `clang`, and I can't get the `RequireIsBaseOf` to work with clang at all. So I assume you work with clang? Can you post a complete working example for clang? – t.niese May 29 '18 at 07:38
  • I've been testing with clang 6.0.0 on ubuntu and clang-902.0.39.1 on macOS. – jtbandes May 29 '18 at 07:41
  • 1
    I'm trying to find pertinent parts in "C++ templates, the complete guide", and while I can't say I've found an answer I could pass on, much of the partial instantiation and full instantiation stuff has "compilers are allowed, but not required" tacked onto it. It could be that this is implementation dependent, too. – GeckoGeorge May 29 '18 at 07:44
  • Here's a working example with `RequiresIsBaseOf`: https://godbolt.org/g/JbCJFg – jtbandes May 29 '18 at 07:44
  • Note that both gcc and clang trunk tell me that `std::is_base_of::value` is unused in the very last, erroneous template. It doesn't actually do anything except force the compiler to do full instantiation of Bar. – GeckoGeorge May 29 '18 at 08:00
  • Oh, and gcc compiles it fine, just with a warning. – GeckoGeorge May 29 '18 at 08:04
  • Yeah, I know the very last example doesn't actually function as intended, I just used it to point out what I don't understand — how the incompleteness of T matters sometimes and not others even when it appears in almost the same position in the code. – jtbandes May 29 '18 at 08:07

1 Answers1

1

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.

GeckoGeorge
  • 446
  • 1
  • 3
  • 11
  • Why doesn't this point about dependent names apply to `std::is_base_of::value`? Isn't this also a dependent name? – jtbandes May 29 '18 at 16:54
  • 1
    @jtbandes I would guess that's because `std::is_base_of` internally does not have any code at some point but just a mapping to a pseudo-function that will be interpreted by the compiler. And those pseudo-functions might behave differently. I'm also not sure if this information on [cppreference.com std::is_base_of](http://en.cppreference.com/w/cpp/types/is_base_of) `[...]If both Base and Derived are non-union class types, and they are not the same type (ignoring cv-qualification), Derived shall be a complete type; otherwise the behavior is undefined.[...]` is related to that. – t.niese May 29 '18 at 17:09
  • I could've sworn I saw something about static_assert forcing a complete name, but I can't find it mentioned in the book anymore. What I do know is that std::is_base_of requires complete types (except for some simple cases). I'll edit my answer to reflect that. Edit: oops, forgot to reload. Didn't see t.niese 's comment – GeckoGeorge May 29 '18 at 17:23