Search code examples
c++templatestypedefsfinaeenable-if

std::enable_if_t typedef substitutions aren't equivalent


I often make a this_type typedef for classes to shorten member function signatures. However, I ran across this oddity when using std::enable_if_t.

#include <iostream>
#include <string>
#include <type_traits>    

template <class T, class Enable = void>
struct foo;

template <class T>
struct foo <T, std::enable_if_t<std::is_arithmetic_v<T>>>
{
    T data {};
    //typedef foo<T, void> this_type; // <-- can't do this
    //typedef foo<T, std::enable_if_t<true>> this_type; // <- can't do this either

    typedef foo<T, std::enable_if_t<std::is_arithmetic_v<T>>> this_type; // have to do this
    foo () = default;
    explicit foo(T i): data(i) {}
    foo (this_type const &) = default;
    static constexpr auto isame = std::is_same_v<this_type, foo<T, void>>;
};

You can't use completely equivalent types in the definition of this_type -- the error is, of course, that foo (this_type const &) can't be defaulted because this_type doesn't match the actual class.

And yet they are all the same type:

int main(void)
{
    std::boolalpha(std::cout);
    std::cout << "enable_if_t: " << typeid(std::enable_if_t<std::is_arithmetic_v<int>>).name() << '\n';
    foo<int> myfoo{1};
    std::cout << "Same: " << std::is_same_v<decltype(myfoo), foo<int, void>> << '\n';
    std::cout << "ISame: " << decltype(myfoo)::isame << '\n';
    std::cout << typeid(decltype(myfoo)).name() << '\n';
    std::cout << typeid(foo<int, void>).name() << '\n';
    std::cout << typeid(foo<int>::this_type).name() << '\n';

    foo<int> myfoo2(myfoo);
    return 0;
}

GCC output:

enable_if_t: v
Same: true
ISame: true
3fooIivE
3fooIivE
3fooIivE

And the compiler should know this as soon as the specialization succeeds: The result of std::enable_if_t is immediately replaced by the type, in this case void. And the sample output bears it out.

I thought this might be related to problems created by an old C++ standard between typedefs of the same type at namespace scope, but this is is not the cause, because otherwise typedef foo<T, std::enable_if_t<true>> this_type would have worked.

What is preventing the shorter type aliases from being recognized?


It's been pointed out in a comment that the shorter aliases work in clang. However, MSVC's behavior is the same as gcc.


Solution

  • It's a compiler bug in GCC (and MSVC).

    The compiler is not permitted to preemptively reject your template declaration just because it cannot figure out whether the member declaration foo (this_type const &) = default; is always going to be legal for each instantiation. If there is a possibility of an ill-formed instantiation, it must reject the program if such an ill-formed instantiation actually occurs (i.e. one for which this_type is not the same type as the enclosing class type, making this signature not eligible for defaulting). The compiler can also reject the program if it can tell that all instantiations would be ill-formed, but that isn't the case for your code. See [temp.res.general]/6.

    The determination of whether a constructor of a class template is a copy constructor is not made until the class template is actually instantiated. At that point, the answer is always known because under [class.copy.ctor]/1, only a non-template constructor can be a copy constructor (though template constructors can still be selected by overload resolution to perform copies). So, when you define a class template foo, and that class template contains a non-template constructor C, the compiler cannot always immediately know whether C will be a copy constructor, since the signature of C might contain a dependent type. However, when the compiler instantiates some foo<T>, then the concrete type of the member C of foo<T> will be known (since T has been supplied) which means, at that point, the compiler can tell whether or not C is a copy constructor. Again, this is made possible by the fact that the copy constructor cannot have any of its own template parameters and still be considered a copy constructor; so its signature is known as soon as the enclosing concrete class type is known.