Search code examples
c++sfinaeenable-ifc++-templates

Why class template instantiation fails for unused member template function and implicit instantiation of member function's declaration


Whats wrong with this:

#include <type_traits>

struct A;

template<typename T>
struct B
{
    template<typename=std::enable_if<std::is_copy_constructible<T>::value>>
    void f1() {}
};

template<typename T>
struct C {};


// Type your code here, or load an example.
int main() {
    // Following fails
    B<A> b;
    // Could use this:
    // b.f1<C>();

    // This complies
    C<A> c;

    return 0;
}

/* This to be in or not doesn't make a difference
struct A
{};
*/

I tried this here: https://godbolt.org/z/NkL44s with different compilers:

  • x86-64 gcc 9.2: compiles
  • x86-64 gcc (trunk): fails
  • x86-64 clang 6.0.0: compiles
  • x86-64 clang 7.0.0 and later: fails
  • x64 msvc v19.22: compiles
  • x64 msvc v19.23 (tested internally): fails

So why do more recent compilers reject this? When instantiating B<A> it is not clear in which form f1 will be used or if it will be used at all. So why the compiler complains about it? Shouldn't the f1 member template function be checked only if it is really used?


Edit:
As mentioned in comments, I made an unintented mistake in above code: std::enable_if should have been std::enable_if_t, as in this corrected playground: https://godbolt.org/z/cyuB3d

This changes the picture of compilers passing this code without error:

  • gcc: fails
  • clang: fails
  • x64 msvc v19.22: compiles
  • x64 msvc v19.23 (tested internally): fails

However, the question remains: Why does a defaulted template parameter of a function that is never used lead to compilation failure?


Solution

  • When instantiating the class template B<A>, the compiler instantiates the declaration, but not the definition of B<A>::f1:

    The implicit instantiation of a class template specialization causes

    • the implicit instantiation of the declarations, but not of the definitions, of the non-deleted class member functions, member classes, scoped member enumerations, static data members, member templates, and friends; and
    • [...]

    The implicit instantiation of a class template specialization does not cause the implicit instantiation of default arguments or noexcept-specifiers of the class member functions.

    - [temp.inst] p3

    However, that just means that the function body isn't instantiated; the rest is. If f1 had default arguments, those wouldn't be instantiated, but the std::enable_if is a default template argument so this exception doesn't apply.

    The following gets instantiated:

    template<typename = std::enable_if<std::is_copy_constructible<A>::value>>
    void f1() {}
    

    The default template argument is ill-formed because std::is_copy_constructible requires:

    T shall be a complete type, cv void, or an array of unknown bound.

    Solution

    In C++20, using a trailing requires-clause and the std::copy_constructible concept:

    void f1() requires std::copy_constructible<T> {}
    

    In C++17:

    template<typename U = T>
    std::enable_if_t<std::is_copy_constructible_v<U>> f1() {}
    

    This works because std::enable_if_t depends on a template parameter to the current function template, so SFINAE can take place as intended.

    See std::enable_if to conditionally compile a member function for more strategies.