Search code examples
c++c++17sfinaetype-traitsconversion-operator

Conversion functions, std::is_base_of and spurious incomplete types: substitution failure IS an error


I'm attempting to implement a conversion function operator, and use std::is_base_of to limit the scope of applicability, but I'm running into issues.

#include <type_traits>

class Spurious;

class MyClassBase {};

template< typename T >
class MyClass: public MyClassBase {
public:
    template< typename U, std::enable_if_t< std::is_base_of<MyClassBase, U>::value, bool> = true >
    operator U () const {
        return U{}; // Complex initialization omitted for brevity
    }
};

class MyBool: public MyClass< bool > {};
class MyInt: public MyClass< int > {};

int do_stuff( int i, Spurious const & obj);
int do_stuff( int i, MyBool const & obj) {
    return i;
}

int main() {
    MyInt const obj;
    return do_stuff( 3, obj );
}

The presence of Spurious and the related function definitions means that I'm getting compiler errors (compiler explorer; GCC: error: invalid use of incomplete type 'const class Spurious' Clang:error: incomplete type 'Spurious' used in type trait expression) in the is_base_of implementation.

I get why you can't is_base_of an incomplete type, but I'm not quite understanding why "substitution failure is not an error" isn't applicable here. I would have thought that attempting to is_base_of an incomplete type during template expansion would cause the compiler to just abort the conversion attempt and move on to the next function definition. That's sort of the entire point of the is_base_of statement here: ignore classes which don't match the pattern.

My main question, though, is not the "why", but the "what next". Are there any workarounds? Is there some way to ignore things like Spurious while not requiring them to be fully defined within this compilation unit? Ideally, I'd be able to just modify the definition of the conversion operator (e.g. the SFINAE template parameters) to get it to work.


Solution

  • This isn’t SFINAE, which certainly applies to incomplete types:

    template<class T,bool=false>
    struct size : std::integral_constant<std::size_t,0> {};
    template<class T>
    struct size<T,!sizeof(T)> : std::integral_constant<std::size_t,sizeof(T)> {};
    static_assert(size<char>::value==1);  // OK
    
    struct A;
    static_assert(size<A>::value==0);  // OK
    struct A {};
    static_assert(size<A>::value==0);  // OK!
    

    The issue here is that std::is_base_of has “undefined behavior” (read: use of it makes a program ill-formed, no diagnostic required) if both types are non-union class types and the latter is incomplete. This isn’t covered by SFINAE both because it’s not in the immediate context and because it’s NDR.

    These compilers are choosing to reject the code, which has the benefit of preventing surprises like the // OK! above.

    You can use the direct SFINAE approach to avoid this problem:

    template< typename T >
    class MyClass: public MyClassBase {
    public:
        template< typename U, std::enable_if_t<
            (sizeof(U), std::is_base_of<MyClassBase, U>::value), bool> = true >
        operator U () const {
            return U{}; // Complex initialization omitted for brevity
        }
    };
    

    Here there is a direct substitution error in the type of the template parameter if U is incomplete, which prevents instantiating the std::is_base_of specialization that would be IFNDR. Note that using incomplete types with SFINAE this way is still rather dangerous, because the same specialization instantiated in a context where the types in question are complete might produce different behavior (which is also IFNDR, per [temp.point]/7).